Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ instance teardown, and the dev-tools loop. This section is the QuickAdd-specific
brief that a skill-less agent still needs.

- Plugin id `quickadd`. Reload with `obsidian vault=<vault> plugin:reload
id=quickadd`; the runner's ready probe is `quickadd:list`. Trigger the test
action with `obsidian vault=<vault> command id=quickadd:testQuickAdd` (or via
the runner: `pnpm run obsidian:e2e -- command id=quickadd:testQuickAdd`).
id=quickadd`; the runner's ready probe is `quickadd:list`. Run a choice with
`pnpm run obsidian:e2e -- quickadd:run choice=<name>` - note that `ok:true`
means the run finished, not that it did what you wanted. Drive anything else
through `pnpm run obsidian:e2e -- eval code=<javascript>`; top-level `await`
is not allowed there, so wrap async work in an IIFE the eval can await.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- The four runner scripts - `provision:e2e-vault`, `start:e2e-obsidian`,
`stop:e2e-obsidian`, `obsidian:e2e` - run the shared `obsidian-e2e` bin,
configured by `obsidian-e2e.config.mjs` at the repo root (plugin id, the
Expand Down
8 changes: 2 additions & 6 deletions docs/src/content/docs/docs/AIAssistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,8 @@ Use these rules when choosing a value:

### Max chunk tokens {#max-chunk-tokens}

The chunked AI prompt flow has a separate **Max chunk tokens** setting. It
controls the estimated token budget for the text inserted into `{{VALUE:chunk}}`
for each chunk.

The system prompt and prompt template are counted separately. Values above the
selected model's estimated input budget are capped automatically.
Chunk sizing has no setting of its own. It is the `maxChunkTokens` option of
[`chunkedPrompt()`](/docs/QuickAddAPI/#max-chunk-tokens) in the script API.

### Output length {#output-length}

Expand Down
13 changes: 12 additions & 1 deletion docs/src/content/docs/docs/QuickAddAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ results. Use this for inputs that are too large for a single request.
- `chunkSeparator`: `RegExp` used to split `text` (default: `/\n/`)
- `chunkJoiner`: String inserted between chunk results (default: `"\n"`)
- `shouldMerge`: Merge small adjacent chunks up to the budget (default: `true`)
- `maxChunkTokens`: Maximum **estimated** tokens for each chunk's text (the `{{VALUE:chunk}}` portion only - the system prompt and prompt template are budgeted separately). Token counts are estimated locally; values above the model's estimated input budget are capped automatically.
- `maxChunkTokens`: Maximum **estimated** tokens for each chunk's text - see [Max chunk tokens](#max-chunk-tokens) below.

**Behavior:**
- Chunk sizes are estimated locally (QuickAdd no longer bundles model-specific tokenizers); the configured provider remains the source of truth for exact limits.
Expand All @@ -636,6 +636,17 @@ const result = await quickAddApi.ai.chunkedPrompt(
);
```

#### Max chunk tokens {#max-chunk-tokens}

`maxChunkTokens` is the estimated token budget for the text inserted into
`{{VALUE:chunk}}` for each chunk. It is an option of this API call, not a
QuickAdd setting - there is no **Max chunk tokens** field anywhere in the
plugin's settings or in the Macro AI Assistant command.

The system prompt and prompt template are counted separately. Token counts are
estimated locally, and values above the selected model's estimated input budget
are capped automatically.

### Give the model tools to call: `ai.agent(config)` {#tool--function-calling--aiagentconfig}

_Introduced in QuickAdd 2.14.0._
Expand Down
5 changes: 3 additions & 2 deletions src/ai/AIAssistant.systemPromptLiteral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import type { CommonResponse } from "./OpenAIRequest";
*
* If #1572 ever makes system prompts formattable, this test fails, and whoever
* changes it is the person who should also restore the preview and the token
* autocomplete in AIAssistantSettingsModal / AIAssistantCommandSettingsModal /
* AIAssistantInfiniteCommandSettingsModal.
* autocomplete in AIAssistantSettingsModal / AIAssistantCommandSettingsModal.
* (A third modal, AIAssistantInfiniteCommandSettingsModal, carried the same
* affordance; it went with the unreachable command type it configured, #1571.)
*/

const storeState = vi.hoisted(() => ({
Expand Down
93 changes: 0 additions & 93 deletions src/ai/aiHelpers.maxChunkTokens.test.ts

This file was deleted.

42 changes: 0 additions & 42 deletions src/ai/aiHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { log } from "src/logger/logManager";
import { settingsStore } from "src/settingsStore";
import type { AIProvider, Model, ModelRef } from "./Provider";
import { estimateModelInputBudget } from "./tokenEstimator";

/** Conservative context-window fallback when no model is known or configured. */
const FALLBACK_MODEL_MAX_TOKENS = 4096;

/** A model together with the provider that will serve it. */
export interface ResolvedModel {
Expand Down Expand Up @@ -242,41 +238,3 @@ export function getModelMaxTokens(model: ModelInput) {
);
}

/**
* Largest context window among all configured models, or a conservative
* fallback when none are configured. Used when the selected model is unknown at
* config time (see getMaxChunkTokensUpperBound).
*/
export function getLargestModelMaxTokens(): number {
const aiSettings = settingsStore.getState().ai;

const tokens = aiSettings.providers
.flatMap((provider) => provider.models)
.map((model) => model.maxTokens)
.filter((value) => Number.isFinite(value) && value > 0);

return tokens.length ? Math.max(...tokens) : FALLBACK_MODEL_MAX_TOKENS;
}

/**
* Upper bound for the "Max chunk tokens" slider: the model's estimated input
* budget minus the system-prompt overhead, floored at 1.
*
* The selected model can be unknown at config time — the "Ask me" sentinel
* (resolved at runtime) or a model that was removed from settings. In that case
* we fall back to the most permissive configured model instead of throwing,
* which would blank the settings modal. The runtime still caps each chunk to the
* actual model's budget, so a generous UI bound never lets a request exceed the
* real limit.
*/
export function getMaxChunkTokensUpperBound(
model: ModelInput,
systemPromptTokens: number,
): number {
// Silent: this feeds a settings-modal slider bound; rendering a modal must
// not consume the warn-once budget that belongs to an actual run.
const resolved = resolveModel(model, { silent: true });
const maxTokens = resolved?.model.maxTokens ?? getLargestModelMaxTokens();

return Math.max(1, estimateModelInputBudget(maxTokens) - systemPromptTokens);
}
5 changes: 1 addition & 4 deletions src/ai/modelRefPinning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ type AICommandish = ICommand & {
};

function isAICommand(command: ICommand): command is AICommandish {
return (
command.type === CommandType.AIAssistant ||
command.type === CommandType.InfiniteAIAssistant
);
return command.type === CommandType.AIAssistant;
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/commandLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@ export const QUICK_ADD_COMMAND_LABELS = {
runTemplateFromFolder: "New note from template",
applyTemplate: "Apply template to active note",
reloadDev: "Reload (dev)",
testDev: "Test (dev)",
openSettings: "Open settings",
} as const;
42 changes: 26 additions & 16 deletions src/engine/MacroChoiceEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,31 @@ function getUserScriptSettings(
return isRecord(settings) ? settings : undefined;
}

/**
* Command types QuickAdd used to declare and no longer does. Without this, the
* generic message would tell a user whose data.json holds one of these that it
* "can come from a newer version of QuickAdd" - the opposite of the truth, and
* no help at all. A removed type is a fact we know; say the known thing.
*
* A Map, not an object literal: the key comes straight from a hand-edited
* data.json, and `{}["constructor"]` is not a miss.
*/
const RETIRED_COMMAND_TYPES = new Map<string, string>([
[
"InfiniteAIAssistant",
'QuickAdd has removed the "Infinite AI Assistant" command type - no released version could ever run one. Delete the step from the macro. For chunked AI prompts, use quickAddApi.ai.chunkedPrompt() in a user script.',
],
]);

/**
* Why a command could not be run, phrased for the person reading the notice.
*
* Two shapes reach here, and telling them apart is the difference between
* actionable and baffling. A real-but-unknown type name is worth quoting: it is
* what the user greps for. An entry with no usable type has nothing to quote -
* printing `'undefined'` or `'[object Object]'` (a hand-authored package can put
* any JSON here) would send them looking for a command type that never existed.
* Three shapes reach here, and telling them apart is the difference between
* actionable and baffling. A type QuickAdd used to declare gets the truth about
* where it came from. Any other real type name is worth quoting: it is what the
* user greps for. An entry with no usable type has nothing to quote - printing
* `'undefined'` or `'[object Object]'` (a hand-authored package can put any JSON
* here) would send them looking for a command type that never existed.
*/
function describeUnknownType(
type: unknown,
Expand All @@ -114,6 +131,10 @@ function describeUnknownType(
return `the saved entry has no ${kind} type, so QuickAdd cannot tell what it was meant to do. It is in .obsidian/plugins/quickadd/data.json.`;
}

const retired =
kind === "command" ? RETIRED_COMMAND_TYPES.get(type) : undefined;
if (retired) return retired;

return `QuickAdd does not recognise the ${kind} type '${type}'. It can come from a newer version of QuickAdd, an imported package, or a hand-edited .obsidian/plugins/quickadd/data.json.`;
}

Expand Down Expand Up @@ -318,17 +339,6 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
case CommandType.AIAssistant:
await this.executeAIAssistant(command as IAIAssistantCommand);
break;
case CommandType.InfiniteAIAssistant:
// Never creatable, never executable: it shipped in 1.2.0 with no
// builder entry and no engine branch, so the only way to hold one
// is a hand-edited data.json or a hand-authored package. Naming it
// here is what the `never` check costs, and it turns the silent
// drop into a visible one until the type is removed outright.
this.reportUnrunnableCommand(
command,
'no released QuickAdd has ever been able to run an "Infinite AI Assistant" command - it cannot be created or configured in the macro builder either. Delete the step from the macro.',
);
break;
case CommandType.OpenFile:
await this.executeOpenFile(command as IOpenFileCommand);
break;
Expand Down
9 changes: 7 additions & 2 deletions src/engine/MacroChoiceEngine.unknownCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,15 @@ describe("MacroChoiceEngine over a command type it cannot run (#1571)", () => {
expect(errors).toHaveLength(1);
});

it("shouts about the Infinite AI Assistant command, which never ran anywhere", async () => {
it("tells the truth about a retired type, not the newer-version guess", async () => {
// A real legacy string, exactly as 2.19.x and earlier wrote it. The
// generic message would say it "can come from a newer version of
// QuickAdd" - the opposite of the truth for a type we removed ourselves.
const { run, executeCommandById } = runMacro([
{
id: "c1",
name: "Summarise",
type: CommandType.InfiniteAIAssistant,
type: "InfiniteAIAssistant",
model: "gpt-4",
outputVariableName: "out",
},
Expand All @@ -141,6 +144,8 @@ describe("MacroChoiceEngine over a command type it cannot run (#1571)", () => {
expect(errors).toHaveLength(1);
expect(errors[0]).toContain("Summarise");
expect(errors[0]).toMatch(/Infinite AI Assistant/i);
expect(errors[0]).toContain("chunkedPrompt");
expect(errors[0]).not.toMatch(/newer version/i);
expect(executeCommandById).toHaveBeenCalledWith(OBSIDIAN_COMMAND_ID);
});

Expand Down
Loading