@vellumai/assistant 0.8.9-staging.1 → 0.8.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/openapi.yaml +6 -7
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +33 -18
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +19 -16
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +48 -29
- package/src/__tests__/conversation-agent-loop.test.ts +48 -96
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +52 -49
- package/src/__tests__/memory-retrieval-hook.test.ts +68 -30
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +12 -7
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -40
- package/src/acp/__tests__/session-manager-resume.test.ts +179 -27
- package/src/acp/auto-install.test.ts +130 -46
- package/src/acp/auto-install.ts +86 -31
- package/src/acp/prepare-agent-env.ts +8 -9
- package/src/acp/resolve-agent.test.ts +26 -123
- package/src/acp/resolve-agent.ts +26 -85
- package/src/acp/resume-hint.ts +1 -3
- package/src/acp/session-manager.ts +32 -20
- package/src/acp/types.ts +0 -8
- package/src/agent/loop.ts +42 -26
- package/src/config/bundled-skills/acp/SKILL.md +14 -14
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
- package/src/config/bundled-skills/app-builder/TOOLS.json +43 -2
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/daemon/conversation-agent-loop.ts +15 -57
- package/src/daemon/conversation.ts +23 -3
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +0 -7
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +13 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +6 -23
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +24 -25
- package/src/runtime/routes/__tests__/acp-routes.test.ts +71 -39
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +29 -4
- package/src/runtime/routes/plugins-routes.ts +10 -9
- package/src/tools/acp/list-agents.test.ts +2 -2
- package/src/tools/acp/spawn.test.ts +103 -225
- package/src/tools/acp/spawn.ts +10 -120
- package/src/tools/apps/executors.ts +153 -42
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
package/src/agent/loop.ts
CHANGED
|
@@ -10,13 +10,11 @@ import {
|
|
|
10
10
|
estimateToolsTokens,
|
|
11
11
|
getCalibrationProviderKey,
|
|
12
12
|
} from "../context/token-estimator.js";
|
|
13
|
-
import type {
|
|
14
|
-
ContextWindowManager,
|
|
15
|
-
ContextWindowResult,
|
|
16
|
-
} from "../context/window-manager.js";
|
|
13
|
+
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
17
14
|
import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
|
|
18
15
|
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
19
16
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
17
|
+
import { stripHistoricalWebSearchResults } from "../daemon/web-search-history.js";
|
|
20
18
|
import { HOOKS } from "../plugin-api/constants.js";
|
|
21
19
|
import type {
|
|
22
20
|
AssistantMessageContext,
|
|
@@ -28,6 +26,7 @@ import {
|
|
|
28
26
|
DEFAULT_COMPACTION_PLUGIN_NAME,
|
|
29
27
|
defaultCompact,
|
|
30
28
|
} from "../plugins/defaults/compaction/compact.js";
|
|
29
|
+
import { getContextWindowManager } from "../plugins/defaults/compaction/manager-store.js";
|
|
31
30
|
import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
32
31
|
import { runHook } from "../plugins/pipeline.js";
|
|
33
32
|
import type { CompactionCircuitEvent } from "../plugins/types.js";
|
|
@@ -467,15 +466,6 @@ export interface AgentLoopRunOptions {
|
|
|
467
466
|
* compactor fail-closes to excluding guardian-only attachments.
|
|
468
467
|
*/
|
|
469
468
|
trust: TrustContext;
|
|
470
|
-
/**
|
|
471
|
-
* The conversation's {@link ContextWindowManager}, supplied by the
|
|
472
|
-
* orchestrator so mid-loop in-place compaction can run against the real
|
|
473
|
-
* per-conversation manager. Absent for callers without a compaction path
|
|
474
|
-
* (agent wakes, standalone unit tests); the loop only reads it when the
|
|
475
|
-
* mid-loop budget gate trips with {@link AgentLoopRunOptions.compactInPlace}
|
|
476
|
-
* enabled.
|
|
477
|
-
*/
|
|
478
|
-
contextWindowManager?: ContextWindowManager;
|
|
479
469
|
/**
|
|
480
470
|
* Ad-hoc inference-profile override applied to every LLM call the loop
|
|
481
471
|
* issues. When set, each `SendMessageOptions.config` carries
|
|
@@ -726,7 +716,6 @@ export class AgentLoop {
|
|
|
726
716
|
history: Message[],
|
|
727
717
|
requestId: string | undefined,
|
|
728
718
|
trust: TrustContext,
|
|
729
|
-
manager: ContextWindowManager | undefined,
|
|
730
719
|
signal: AbortSignal | undefined,
|
|
731
720
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
732
721
|
overrideProfile: string | null,
|
|
@@ -741,9 +730,14 @@ export class AgentLoop {
|
|
|
741
730
|
// Record the history-stripped marker right after stripping, before the
|
|
742
731
|
// pipeline runs.
|
|
743
732
|
await onEvent({ type: "history_stripped" });
|
|
733
|
+
// The compaction module owns the per-conversation manager; resolve it from
|
|
734
|
+
// the store rather than holding a handle on the loop. Absent for callers
|
|
735
|
+
// without a compaction path (agent wakes, standalone unit tests), which
|
|
736
|
+
// never reach this gate.
|
|
737
|
+
const manager = getContextWindowManager(this.conversationId);
|
|
744
738
|
if (manager == null) {
|
|
745
739
|
throw new PluginExecutionError(
|
|
746
|
-
|
|
740
|
+
`default-compaction: no ContextWindowManager registered for conversation ${this.conversationId} — the compaction store must construct one before compaction runs`,
|
|
747
741
|
DEFAULT_COMPACTION_PLUGIN_NAME,
|
|
748
742
|
);
|
|
749
743
|
}
|
|
@@ -814,7 +808,6 @@ export class AgentLoop {
|
|
|
814
808
|
resolveOverrideProfile,
|
|
815
809
|
resolveContextWindow,
|
|
816
810
|
compactInPlace = false,
|
|
817
|
-
contextWindowManager,
|
|
818
811
|
isNonInteractive = false,
|
|
819
812
|
modelProfile = null,
|
|
820
813
|
actorContext = null,
|
|
@@ -940,7 +933,6 @@ export class AgentLoop {
|
|
|
940
933
|
history,
|
|
941
934
|
requestId,
|
|
942
935
|
trust,
|
|
943
|
-
contextWindowManager,
|
|
944
936
|
signal,
|
|
945
937
|
onEvent,
|
|
946
938
|
resolveEffectiveOverrideProfile() ?? null,
|
|
@@ -1115,15 +1107,10 @@ export class AgentLoop {
|
|
|
1115
1107
|
);
|
|
1116
1108
|
rlog.info({ turn: toolUseTurns }, "LLM call start");
|
|
1117
1109
|
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
// Also strip old AX tree snapshots to keep TTFT from growing
|
|
1123
|
-
// linearly with step count in computer-use sessions.
|
|
1124
|
-
const providerHistory = compactAxTreeHistory(
|
|
1125
|
-
stripOldMediaBlocks(history),
|
|
1126
|
-
);
|
|
1110
|
+
// Sanitize the outbound history right before sending: drop accumulated
|
|
1111
|
+
// media, collapse old AX-tree snapshots, and convert historical
|
|
1112
|
+
// web-search results to text. See {@link preModelCallSanitize}.
|
|
1113
|
+
const providerHistory = preModelCallSanitize(history);
|
|
1127
1114
|
|
|
1128
1115
|
// A `pre-model-call` hook (below) can defer this turn's assistant
|
|
1129
1116
|
// output; when set, the live text stream is held so an
|
|
@@ -1936,3 +1923,32 @@ function stripOldMediaBlocks(history: Message[]): Message[] {
|
|
|
1936
1923
|
};
|
|
1937
1924
|
});
|
|
1938
1925
|
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Sanitize the outbound history immediately before a provider call, bundling
|
|
1929
|
+
* the pre-send transforms the loop applies to every request:
|
|
1930
|
+
* - {@link stripOldMediaBlocks} drops accumulated screenshot/audio bytes from
|
|
1931
|
+
* older tool results — the model saw the media on the turn it was captured.
|
|
1932
|
+
* - {@link compactAxTreeHistory} collapses all but the most recent few
|
|
1933
|
+
* `<ax-tree>` snapshots so TTFT does not grow linearly with step count.
|
|
1934
|
+
* - {@link stripHistoricalWebSearchResults} converts historical
|
|
1935
|
+
* `web_search_tool_result` blocks to text summaries; Anthropic's opaque
|
|
1936
|
+
* `encrypted_content` tokens expire / are route-scoped, and replaying a stale
|
|
1937
|
+
* one is rejected with `Invalid encrypted_content in search_result block`.
|
|
1938
|
+
*
|
|
1939
|
+
* Transforms the outbound copy only — the durable history keeps the rich
|
|
1940
|
+
* originals and each send re-derives the sanitized projection (every transform
|
|
1941
|
+
* is idempotent). Because it runs unconditionally before every provider call,
|
|
1942
|
+
* it is the single place where oversized media and expired web-search tokens
|
|
1943
|
+
* are guaranteed to be removed from a request.
|
|
1944
|
+
*
|
|
1945
|
+
* This is outbound-request preparation and should eventually move to a default
|
|
1946
|
+
* `pre-model-call` plugin hook ({@link HOOKS.PRE_MODEL_CALL}) once that hook's
|
|
1947
|
+
* context carries the outbound message list; for now it lives inline next to
|
|
1948
|
+
* the provider call it guards.
|
|
1949
|
+
*/
|
|
1950
|
+
export function preModelCallSanitize(history: Message[]): Message[] {
|
|
1951
|
+
const mediaStripped = stripOldMediaBlocks(history);
|
|
1952
|
+
const axCompacted = compactAxTreeHistory(mediaStripped);
|
|
1953
|
+
return stripHistoricalWebSearchResults(axCompacted).messages;
|
|
1954
|
+
}
|
|
@@ -43,20 +43,20 @@ When the user first tries to use ACP and it's not enabled, set it up automatical
|
|
|
43
43
|
|
|
44
44
|
2. Then retry the `acp_spawn` call. Do NOT run `vellum sleep && vellum wake` - that kills the conversation.
|
|
45
45
|
|
|
46
|
-
No manual binary installation is needed first: missing adapter binaries are
|
|
46
|
+
No manual binary installation is needed first: missing adapter binaries are installed automatically (see below).
|
|
47
47
|
|
|
48
48
|
## Automatic adapter availability
|
|
49
49
|
|
|
50
|
-
When `acp_spawn` finds the agent's binary missing from PATH, the assistant
|
|
50
|
+
When `acp_spawn` finds the agent's binary missing from PATH, the assistant installs it once via a sandboxed bun global install and then runs the real installed binary. The install runs in a fresh empty temporary directory (never the task's project directory), with the Claude/Gemini secrets stripped from the installer environment and the registry pinned to the public npm registry, so a malicious project directory cannot hijack package resolution or capture a token. After this one-time install, the adapter is a normal trusted binary on PATH and every later spawn (and resume) uses it directly.
|
|
51
51
|
|
|
52
|
-
Only the allowlisted out-of-box packages are ever
|
|
52
|
+
Only the allowlisted out-of-box packages are ever installed this way (`@agentclientprotocol/claude-agent-acp`, `@zed-industries/codex-acp`, `@google/gemini-cli`); user-configured agents with custom commands are never installed automatically.
|
|
53
53
|
|
|
54
|
-
Manual installation is fallback guidance for unusual setups: bun
|
|
54
|
+
Manual installation is fallback guidance for unusual setups: bun unavailable, restricted global installs, or an auto-install failure (the failure reason is surfaced in the tool result).
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
bun add -g @agentclientprotocol/claude-agent-acp # claude
|
|
58
|
+
bun add -g @zed-industries/codex-acp # codex
|
|
59
|
+
bun add -g @google/gemini-cli # gemini
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## Claude setup
|
|
@@ -71,7 +71,7 @@ When the token is missing, do NOT ask the user to paste it into chat. Collect it
|
|
|
71
71
|
|
|
72
72
|
## Codex setup
|
|
73
73
|
|
|
74
|
-
The `codex-acp` adapter is
|
|
74
|
+
The `codex-acp` adapter is installed automatically when missing, but it shells out to the underlying `codex` CLI, which must also be on PATH:
|
|
75
75
|
|
|
76
76
|
1. **Install the Codex CLI** (version 0.111 or higher) via OpenAI's distribution channel of choice. The adapter will fail if `codex` isn't on PATH.
|
|
77
77
|
|
|
@@ -82,7 +82,7 @@ The `codex-acp` adapter is fetched automatically when missing, but it shells out
|
|
|
82
82
|
|
|
83
83
|
## Gemini setup
|
|
84
84
|
|
|
85
|
-
Gemini CLI speaks ACP natively (`gemini --acp`) - there is no separate adapter binary. The CLI itself is
|
|
85
|
+
Gemini CLI speaks ACP natively (`gemini --acp`) - there is no separate adapter binary. The CLI itself is installed from `@google/gemini-cli` when missing.
|
|
86
86
|
|
|
87
87
|
**Authenticate** with an API key through the credential store (the primary path):
|
|
88
88
|
|
|
@@ -120,14 +120,14 @@ A workspace `acp.agents.gemini` override is only for non-secret customization (c
|
|
|
120
120
|
|
|
121
121
|
## Updating an adapter
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
Adapters are installed once via a bun global install. To update one to the latest version, ask the user first, then re-install it globally:
|
|
124
124
|
|
|
125
125
|
```bash
|
|
126
|
-
|
|
126
|
+
bun add -g @agentclientprotocol/claude-agent-acp@latest
|
|
127
127
|
# or
|
|
128
|
-
|
|
128
|
+
bun add -g @zed-industries/codex-acp@latest
|
|
129
129
|
# or
|
|
130
|
-
|
|
130
|
+
bun add -g @google/gemini-cli@latest
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
Then retry the `acp_spawn` call.
|
|
@@ -140,7 +140,7 @@ Then retry the `acp_spawn` call.
|
|
|
140
140
|
|
|
141
141
|
## Discoverability
|
|
142
142
|
|
|
143
|
-
Use `acp_list_agents` to see what's set up and what's missing. It returns each available agent profile, whether ACP is enabled, whether the agent
|
|
143
|
+
Use `acp_list_agents` to see what's set up and what's missing. It returns each available agent profile, whether ACP is enabled, whether the agent's binary is on PATH (missing binaries are installed automatically on first spawn), and an install hint if not. This is the right tool to call when deciding between `claude`, `codex`, and `gemini`, or when the user asks "what coding agents do I have?"
|
|
144
144
|
|
|
145
145
|
## Working directory
|
|
146
146
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"tools": [
|
|
4
4
|
{
|
|
5
5
|
"name": "acp_spawn",
|
|
6
|
-
"description": "Spawn an external coding agent (e.g. Claude Code, Codex, Gemini) via ACP to work on a task. Default profiles ship for `claude` (`claude-agent-acp`), `codex` (`codex-acp`), and `gemini` (`gemini --acp`); the assistant resolves the agent id to the right binary. The agent runs as a subprocess and streams results back. Use this when you want to delegate a coding task to an external agent that has its own tools, file editing, and terminal access. If a default agent's binary is missing, the assistant
|
|
6
|
+
"description": "Spawn an external coding agent (e.g. Claude Code, Codex, Gemini) via ACP to work on a task. Default profiles ship for `claude` (`claude-agent-acp`), `codex` (`codex-acp`), and `gemini` (`gemini --acp`); the assistant resolves the agent id to the right binary. The agent runs as a subprocess and streams results back. Use this when you want to delegate a coding task to an external agent that has its own tools, file editing, and terminal access. If a default agent's binary is missing, the assistant installs it once via a sandboxed bun global install and proceeds in the same call; if that fails (e.g. bun unavailable), an actionable install hint is returned - do NOT alter `agents.<id>.command` to swap binaries. If ACP is disabled, follow the setup instructions in SKILL.md.",
|
|
7
7
|
"category": "orchestration",
|
|
8
8
|
"risk": "high",
|
|
9
9
|
"input_schema": {
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"name": "acp_list_agents",
|
|
90
|
-
"description": "Lists ACP coding agents available to spawn. Each entry includes whether ACP is enabled, whether the agent
|
|
90
|
+
"description": "Lists ACP coding agents available to spawn. Each entry includes whether ACP is enabled, whether the agent's binary is on PATH (missing binaries are installed automatically on first spawn), and an install command if not. Use this to decide between 'claude', 'codex', and 'gemini' or to surface setup steps to the user.",
|
|
91
91
|
"category": "orchestration",
|
|
92
92
|
"risk": "low",
|
|
93
93
|
"input_schema": {
|
|
@@ -203,7 +203,7 @@ render(<App />, document.getElementById("app")!);
|
|
|
203
203
|
|
|
204
204
|
#### `app_create` accepts EXACTLY these 7 keys — nothing else
|
|
205
205
|
|
|
206
|
-
`name` (
|
|
206
|
+
`name` (optional — defaults to the `preview` title, else "New App"), `description`, `schema_json`, `source_files`, `preview`, `auto_open`, `change_summary`.
|
|
207
207
|
|
|
208
208
|
Anything else fails with `Invalid input for tool "app_create": Unknown parameter "X"`. The retired keys models still reach for:
|
|
209
209
|
|
|
@@ -251,10 +251,10 @@ Editing an existing app means reusing its `app_id` — never `app_create`. Resol
|
|
|
251
251
|
|
|
252
252
|
- **`file_edit`** — targeted changes (styles, fixes, small features), full path `/workspace/data/apps/<slug>/src/...`
|
|
253
253
|
- **`file_write`** — new files or full rewrites
|
|
254
|
-
-
|
|
254
|
+
- **`app_update`** — rename or change description/schema, and/or rewrite files via `source_files`, in one call. It recompiles for you, so no separate `app_refresh` is needed. Use this instead of editing `<slug>.json` by hand.
|
|
255
255
|
- **Full rebrand** — still iteration, edit the existing files.
|
|
256
256
|
|
|
257
|
-
Then `app_refresh(app_id)` ONCE. If the change is substantial, `app_open(app_id, open_mode: "preview")` for a fresh card; for small tweaks the existing card stays valid.
|
|
257
|
+
Then `app_refresh(app_id)` ONCE (unless you used `app_update`, which already recompiled). If the change is substantial, `app_open(app_id, open_mode: "preview")` for a fresh card; for small tweaks the existing card stays valid.
|
|
258
258
|
|
|
259
259
|
> ⚠️ **`skill_load("app-builder")` is required before every `app_*` call** (including the first `app_create`). The skill can auto-unload between turns; without the reload, `app_refresh` / `app_open` error with "not currently active." It's idempotent — call it every time.
|
|
260
260
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"properties": {
|
|
12
12
|
"name": {
|
|
13
13
|
"type": "string",
|
|
14
|
-
"description": "
|
|
14
|
+
"description": "Optional name of the app. When omitted, defaults to the preview card title (or \"New App\" if there is none); rename later via app_update."
|
|
15
15
|
},
|
|
16
16
|
"description": {
|
|
17
17
|
"type": "string",
|
|
@@ -87,11 +87,52 @@
|
|
|
87
87
|
"description": "Retired. Multi-page apps route inside the Preact app under src/components/. Passing this returns a guidance error."
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
|
-
"required": [
|
|
90
|
+
"required": []
|
|
91
91
|
},
|
|
92
92
|
"executor": "tools/app-create.ts",
|
|
93
93
|
"execution_target": "host"
|
|
94
94
|
},
|
|
95
|
+
{
|
|
96
|
+
"name": "app_update",
|
|
97
|
+
"description": "Update an existing app: change its name, description, or schema, and/or rewrite source files, then recompile. Use this to iterate on an app you already created instead of calling app_create again (which would make a duplicate). Resolve the app_id with app_list first if you don't have it.",
|
|
98
|
+
"category": "apps",
|
|
99
|
+
"risk": "low",
|
|
100
|
+
"input_schema": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {
|
|
103
|
+
"app_id": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "The ID of the app to update"
|
|
106
|
+
},
|
|
107
|
+
"name": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "New name for the app. Omit to leave the name unchanged."
|
|
110
|
+
},
|
|
111
|
+
"description": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "New description for the app. Omit to leave the description unchanged."
|
|
114
|
+
},
|
|
115
|
+
"schema_json": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "New JSON schema for the app data structure. Omit to leave the schema unchanged."
|
|
118
|
+
},
|
|
119
|
+
"source_files": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"description": "Map of relative file paths to file contents to write under the app directory (e.g. 'src/main.tsx', 'src/components/App.tsx', 'src/styles.css'). Supply changed source here instead of separate file_write calls. The app is recompiled after writing.",
|
|
122
|
+
"additionalProperties": {
|
|
123
|
+
"type": "string"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"change_summary": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"description": "Short summary of what changed, using git conventional commit format (e.g. 'fix: correct totals', 'feat: add dark mode'). Used as the version history entry."
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"required": ["app_id"]
|
|
132
|
+
},
|
|
133
|
+
"executor": "tools/app-update.ts",
|
|
134
|
+
"execution_target": "host"
|
|
135
|
+
},
|
|
95
136
|
{
|
|
96
137
|
"name": "app_delete",
|
|
97
138
|
"description": "Delete a persistent app and all its records.",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { setAppCommitMessage } from "../../../../memory/app-git-service.js";
|
|
2
|
+
import * as appStore from "../../../../memory/app-store.js";
|
|
3
|
+
import type { AppUpdateInput } from "../../../../tools/apps/executors.js";
|
|
4
|
+
import { executeAppUpdate } from "../../../../tools/apps/executors.js";
|
|
5
|
+
import type {
|
|
6
|
+
ToolContext,
|
|
7
|
+
ToolExecutionResult,
|
|
8
|
+
} from "../../../../tools/types.js";
|
|
9
|
+
|
|
10
|
+
export async function run(
|
|
11
|
+
input: Record<string, unknown>,
|
|
12
|
+
context: ToolContext,
|
|
13
|
+
): Promise<ToolExecutionResult> {
|
|
14
|
+
if (typeof input.change_summary === "string" && input.change_summary.trim()) {
|
|
15
|
+
setAppCommitMessage(context.conversationId, input.change_summary.trim());
|
|
16
|
+
}
|
|
17
|
+
return executeAppUpdate(input as unknown as AppUpdateInput, appStore);
|
|
18
|
+
}
|
|
@@ -25,6 +25,7 @@ import * as appDelete from "./bundled-skills/app-builder/tools/app-delete.js";
|
|
|
25
25
|
import * as appGenerateIcon from "./bundled-skills/app-builder/tools/app-generate-icon.js";
|
|
26
26
|
import * as appList from "./bundled-skills/app-builder/tools/app-list.js";
|
|
27
27
|
import * as appRefresh from "./bundled-skills/app-builder/tools/app-refresh.js";
|
|
28
|
+
import * as appUpdate from "./bundled-skills/app-builder/tools/app-update.js";
|
|
28
29
|
// ── app-control ────────────────────────────────────────────────────────────────
|
|
29
30
|
import * as appControlClick from "./bundled-skills/app-control/tools/app-control-click.js";
|
|
30
31
|
import * as appControlCombo from "./bundled-skills/app-control/tools/app-control-combo.js";
|
|
@@ -140,6 +141,7 @@ export const bundledToolRegistry = new Map<string, SkillToolScript>([
|
|
|
140
141
|
|
|
141
142
|
// app-builder
|
|
142
143
|
["app-builder:tools/app-create.ts", appCreate],
|
|
144
|
+
["app-builder:tools/app-update.ts", appUpdate],
|
|
143
145
|
["app-builder:tools/app-delete.ts", appDelete],
|
|
144
146
|
["app-builder:tools/app-refresh.ts", appRefresh],
|
|
145
147
|
["app-builder:tools/app-generate-icon.ts", appGenerateIcon],
|
|
@@ -17,7 +17,6 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
|
|
|
17
17
|
patternScan: { profile: "balanced" },
|
|
18
18
|
narrativeRefinement: { profile: "balanced" },
|
|
19
19
|
callAgent: { profile: "balanced" },
|
|
20
|
-
proactiveArtifactBuild: { profile: "balanced" },
|
|
21
20
|
memoryConsolidation: { profile: "balanced" },
|
|
22
21
|
identityIntro: { profile: "balanced" },
|
|
23
22
|
emptyStateGreeting: { profile: "balanced" },
|
|
@@ -42,7 +41,6 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
|
|
|
42
41
|
},
|
|
43
42
|
|
|
44
43
|
filingAgent: { profile: "cost-optimized" },
|
|
45
|
-
proactiveArtifactDecision: { profile: "cost-optimized" },
|
|
46
44
|
memoryExtraction: { profile: "cost-optimized" },
|
|
47
45
|
memoryRetrieval: { profile: "cost-optimized" },
|
|
48
46
|
memoryRetrospective: { profile: "cost-optimized" },
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
"id": "multi-platform-assistant",
|
|
143
|
-
"scope": "
|
|
143
|
+
"scope": "client",
|
|
144
144
|
"key": "multi-platform-assistant",
|
|
145
145
|
"label": "Multi-Platform Assistant Switcher",
|
|
146
146
|
"description": "Enable the menu-bar assistant switcher for managing multiple platform-hosted assistants (new/switch/retire)",
|
|
@@ -300,20 +300,6 @@ const CATALOG_RECORD: CatalogRecord = {
|
|
|
300
300
|
description: "General-purpose LLM inference call site for skill use.",
|
|
301
301
|
domain: "skills",
|
|
302
302
|
},
|
|
303
|
-
proactiveArtifactDecision: {
|
|
304
|
-
id: "proactiveArtifactDecision",
|
|
305
|
-
displayName: "Proactive Artifact Decision",
|
|
306
|
-
description:
|
|
307
|
-
"Decides what personalized artifact to build for new users based on conversation context.",
|
|
308
|
-
domain: "agentLoop",
|
|
309
|
-
},
|
|
310
|
-
proactiveArtifactBuild: {
|
|
311
|
-
id: "proactiveArtifactBuild",
|
|
312
|
-
displayName: "Proactive Artifact Build",
|
|
313
|
-
description:
|
|
314
|
-
"Builds the personalized artifact in a background conversation with tool access.",
|
|
315
|
-
domain: "agentLoop",
|
|
316
|
-
},
|
|
317
303
|
homeGreeting: {
|
|
318
304
|
id: "homeGreeting",
|
|
319
305
|
displayName: "Home Greeting",
|
package/src/context/compactor.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { optimizeImageForTransport } from "../agent/image-optimize.js";
|
|
|
22
22
|
import type { CompactionConfig } from "../config/schemas/compaction.js";
|
|
23
23
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
24
24
|
import { filterMessagesForUntrustedActor } from "../daemon/message-provenance.js";
|
|
25
|
+
import { stripHistoricalWebSearchResults } from "../daemon/web-search-history.js";
|
|
25
26
|
import {
|
|
26
27
|
getAttachmentContent,
|
|
27
28
|
getAttachmentMetadataForMessage,
|
|
@@ -685,6 +686,25 @@ function extractTextFromResponse(content: ContentBlock[]): string {
|
|
|
685
686
|
.join("\n");
|
|
686
687
|
}
|
|
687
688
|
|
|
689
|
+
// Build the outbound message list for a compaction provider call: convert
|
|
690
|
+
// historical web_search_tool_result blocks to text, then append the
|
|
691
|
+
// summarization instruction at the tail.
|
|
692
|
+
//
|
|
693
|
+
// Anthropic's opaque `encrypted_content` tokens are route-scoped and expire, so
|
|
694
|
+
// replaying a stale one is rejected with `Invalid encrypted_content in
|
|
695
|
+
// search_result block`. Sanitizing here — the single seam every compaction
|
|
696
|
+
// provider call funnels through — keeps that error away from both the
|
|
697
|
+
// assistant-driven and emergency summarization calls. Only this outbound copy
|
|
698
|
+
// is sanitized; tail resolution and the persisted compaction result read the
|
|
699
|
+
// caller's original messages, so durable history keeps the rich blocks. The
|
|
700
|
+
// instruction stays at the tail so the prefix cache stays warm.
|
|
701
|
+
function buildCompactionRequest(
|
|
702
|
+
history: Message[],
|
|
703
|
+
instruction: Message,
|
|
704
|
+
): Message[] {
|
|
705
|
+
return [...stripHistoricalWebSearchResults(history).messages, instruction];
|
|
706
|
+
}
|
|
707
|
+
|
|
688
708
|
export async function runAssistantDrivenCompaction(
|
|
689
709
|
args: CompactionRunArgs,
|
|
690
710
|
): Promise<CompactionRunResult> {
|
|
@@ -718,9 +738,7 @@ export async function runAssistantDrivenCompaction(
|
|
|
718
738
|
manifestText,
|
|
719
739
|
);
|
|
720
740
|
|
|
721
|
-
|
|
722
|
-
// stays warm.
|
|
723
|
-
const requestMessages = [...args.messages, instruction];
|
|
741
|
+
const requestMessages = buildCompactionRequest(args.messages, instruction);
|
|
724
742
|
|
|
725
743
|
let response: ProviderResponse;
|
|
726
744
|
try {
|
|
@@ -1090,7 +1108,7 @@ export async function runEmergencyCompaction(
|
|
|
1090
1108
|
role: "user",
|
|
1091
1109
|
content: [{ type: "text", text: EMERGENCY_COMPACTION_PROMPT }],
|
|
1092
1110
|
};
|
|
1093
|
-
const requestMessages =
|
|
1111
|
+
const requestMessages = buildCompactionRequest(prefix, instruction);
|
|
1094
1112
|
|
|
1095
1113
|
let response: ProviderResponse;
|
|
1096
1114
|
try {
|
|
@@ -76,6 +76,11 @@ import { enqueueMemoryRetrospectiveOnCompaction } from "../memory/memory-retrosp
|
|
|
76
76
|
import { HOOKS } from "../plugin-api/constants.js";
|
|
77
77
|
import type { UserPromptSubmitContext } from "../plugin-api/types.js";
|
|
78
78
|
import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
|
|
79
|
+
import {
|
|
80
|
+
createInitialReducerState,
|
|
81
|
+
reduceContextOverflow,
|
|
82
|
+
type ReducerState,
|
|
83
|
+
} from "../plugins/defaults/compaction/context-overflow-reducer.js";
|
|
79
84
|
import { deepRepairHistory } from "../plugins/defaults/history-repair/terminal.js";
|
|
80
85
|
import userPromptSubmitMemoryRetrieval, {
|
|
81
86
|
type MemoryRetrievalHookContext,
|
|
@@ -98,11 +103,6 @@ import { getWorkspaceGitService } from "../workspace/git-service.js";
|
|
|
98
103
|
import { commitTurnChanges } from "../workspace/turn-commit.js";
|
|
99
104
|
import { cleanAssistantContent } from "./assistant-attachments.js";
|
|
100
105
|
import { resolveOverflowAction } from "./context-overflow-policy.js";
|
|
101
|
-
import {
|
|
102
|
-
createInitialReducerState,
|
|
103
|
-
reduceContextOverflow,
|
|
104
|
-
type ReducerState,
|
|
105
|
-
} from "./context-overflow-reducer.js";
|
|
106
106
|
import type { Conversation } from "./conversation.js";
|
|
107
107
|
import {
|
|
108
108
|
createEventHandlerState,
|
|
@@ -152,7 +152,6 @@ import {
|
|
|
152
152
|
UNSENDABLE_IMAGE_NOTE,
|
|
153
153
|
} from "./persist-unsendable-image.js";
|
|
154
154
|
import { resolveTrustClass, type TrustContext } from "./trust-context.js";
|
|
155
|
-
import { stripHistoricalWebSearchResults } from "./web-search-history.js";
|
|
156
155
|
|
|
157
156
|
const log = getLogger("conversation-agent-loop");
|
|
158
157
|
|
|
@@ -881,16 +880,10 @@ export async function runAgentLoopImpl(
|
|
|
881
880
|
const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
|
|
882
881
|
let currentInjectionMode: InjectionMode = "full";
|
|
883
882
|
const memoryCtx: MemoryRetrievalHookContext = {
|
|
884
|
-
graphMemory: ctx.graphMemory,
|
|
885
|
-
config: getConfig(),
|
|
886
883
|
onEvent,
|
|
887
|
-
isTrustedActor,
|
|
888
884
|
conversationId: ctx.conversationId,
|
|
889
885
|
userMessageId,
|
|
890
886
|
logger: rlog,
|
|
891
|
-
// An external cancel aborts `prepareMemory` instead of letting it run
|
|
892
|
-
// to completion after the turn has already been torn down.
|
|
893
|
-
signal: abortController.signal,
|
|
894
887
|
latestMessages: ctx.messages,
|
|
895
888
|
requestId: reqId,
|
|
896
889
|
mode: currentInjectionMode,
|
|
@@ -907,32 +900,6 @@ export async function runAgentLoopImpl(
|
|
|
907
900
|
// message list downstream.
|
|
908
901
|
let runMessages = memoryCtx.latestMessages;
|
|
909
902
|
|
|
910
|
-
// The `remember` tool handles scratchpad-style memory writes directly to the graph.
|
|
911
|
-
|
|
912
|
-
// Reducer state, tool-token budget, and calibration provider key consumed
|
|
913
|
-
// by the post-rejection convergence loop further down. The tool-token
|
|
914
|
-
// budget is resolved once per turn (the resolved tool set is stable across
|
|
915
|
-
// the turn); the calibration key matches the key recorded by `handleUsage`
|
|
916
|
-
// for wrapper providers (OpenRouter routing to Anthropic → key is
|
|
917
|
-
// `"anthropic"`).
|
|
918
|
-
let reducerState: ReducerState | undefined;
|
|
919
|
-
const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
|
|
920
|
-
const estimationProviderName = getCalibrationProviderKey(ctx.provider);
|
|
921
|
-
|
|
922
|
-
// Replace historical web_search_tool_result blocks with text summaries.
|
|
923
|
-
// The opaque `encrypted_content` tokens Anthropic attaches to each result
|
|
924
|
-
// expire / are route-scoped; replaying a stale token is rejected with
|
|
925
|
-
// `Invalid encrypted_content in search_result block`. Titles + URLs
|
|
926
|
-
// preserve enough context for the model on follow-up turns.
|
|
927
|
-
const webSearchStrip = stripHistoricalWebSearchResults(runMessages);
|
|
928
|
-
if (webSearchStrip.stats.blocksStripped > 0) {
|
|
929
|
-
rlog.info(
|
|
930
|
-
{ phase: "pre_run", ...webSearchStrip.stats },
|
|
931
|
-
"Converted historical web_search_tool_result blocks to text summaries",
|
|
932
|
-
);
|
|
933
|
-
runMessages = webSearchStrip.messages;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
903
|
// user-prompt-submit hook: plugins may transform `runMessages` right
|
|
937
904
|
// before the agent loop receives them. Fires once per user turn at the
|
|
938
905
|
// primary `agentLoop.run` only — the re-entry / retry calls further down
|
|
@@ -957,6 +924,16 @@ export async function runAgentLoopImpl(
|
|
|
957
924
|
);
|
|
958
925
|
runMessages = finalUserPromptCtx.latestMessages;
|
|
959
926
|
|
|
927
|
+
// Reducer state, tool-token budget, and calibration provider key consumed
|
|
928
|
+
// by the post-rejection convergence loop further down. The tool-token
|
|
929
|
+
// budget is resolved once per turn (the resolved tool set is stable across
|
|
930
|
+
// the turn); the calibration key matches the key recorded by `handleUsage`
|
|
931
|
+
// for wrapper providers (OpenRouter routing to Anthropic → key is
|
|
932
|
+
// `"anthropic"`).
|
|
933
|
+
let reducerState: ReducerState | undefined;
|
|
934
|
+
const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
|
|
935
|
+
const estimationProviderName = getCalibrationProviderKey(ctx.provider);
|
|
936
|
+
|
|
960
937
|
const shouldGenerateTitle = isReplaceableTitle(
|
|
961
938
|
getConversation(ctx.conversationId)?.title ?? null,
|
|
962
939
|
);
|
|
@@ -1021,7 +998,6 @@ export async function runAgentLoopImpl(
|
|
|
1021
998
|
onCheckpoint,
|
|
1022
999
|
callSite: turnCallSite,
|
|
1023
1000
|
trust: loopTrust,
|
|
1024
|
-
contextWindowManager: ctx.contextWindowManager,
|
|
1025
1001
|
overrideProfile: turnOverrideProfile,
|
|
1026
1002
|
resolveOverrideProfile: resolveCurrentOverrideProfile,
|
|
1027
1003
|
resolveContextWindow,
|
|
@@ -1085,8 +1061,6 @@ export async function runAgentLoopImpl(
|
|
|
1085
1061
|
// intentionally deferred until there's a concrete plugin-level use case.
|
|
1086
1062
|
const retryRepair = deepRepairHistory(updatedHistory);
|
|
1087
1063
|
runMessages = retryRepair.messages;
|
|
1088
|
-
const retryStrip = stripHistoricalWebSearchResults(runMessages);
|
|
1089
|
-
runMessages = retryStrip.messages;
|
|
1090
1064
|
state.orderingErrorDetected = false;
|
|
1091
1065
|
state.deferredOrderingError = null;
|
|
1092
1066
|
|
|
@@ -1380,14 +1354,6 @@ export async function runAgentLoopImpl(
|
|
|
1380
1354
|
if (isTrustedActor && currentInjectionMode !== "minimal") {
|
|
1381
1355
|
ctx.graphMemory.retrackCachedNodes();
|
|
1382
1356
|
}
|
|
1383
|
-
const convergenceStrip = stripHistoricalWebSearchResults(runMessages);
|
|
1384
|
-
if (convergenceStrip.stats.blocksStripped > 0) {
|
|
1385
|
-
rlog.info(
|
|
1386
|
-
{ phase: "convergence", ...convergenceStrip.stats },
|
|
1387
|
-
"Converted historical web_search_tool_result blocks to text summaries",
|
|
1388
|
-
);
|
|
1389
|
-
runMessages = convergenceStrip.messages;
|
|
1390
|
-
}
|
|
1391
1357
|
state.contextTooLargeDetected = false;
|
|
1392
1358
|
yieldedForBudget = false;
|
|
1393
1359
|
|
|
@@ -1469,14 +1435,6 @@ export async function runAgentLoopImpl(
|
|
|
1469
1435
|
if (isTrustedActor && currentInjectionMode !== "minimal") {
|
|
1470
1436
|
ctx.graphMemory.retrackCachedNodes();
|
|
1471
1437
|
}
|
|
1472
|
-
const fallbackStrip = stripHistoricalWebSearchResults(runMessages);
|
|
1473
|
-
if (fallbackStrip.stats.blocksStripped > 0) {
|
|
1474
|
-
rlog.info(
|
|
1475
|
-
{ phase: "fail_gracefully_compact", ...fallbackStrip.stats },
|
|
1476
|
-
"Converted historical web_search_tool_result blocks to text summaries",
|
|
1477
|
-
);
|
|
1478
|
-
runMessages = fallbackStrip.messages;
|
|
1479
|
-
}
|
|
1480
1438
|
state.contextTooLargeDetected = false;
|
|
1481
1439
|
|
|
1482
1440
|
updatedHistory = await runAgentLoop(runMessages);
|