@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.
Files changed (62) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/openapi.yaml +6 -7
  3. package/package.json +1 -1
  4. package/src/__tests__/agent-loop-exit-reason.test.ts +33 -18
  5. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  6. package/src/__tests__/app-executors.test.ts +132 -0
  7. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  8. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  9. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +19 -16
  10. package/src/__tests__/conversation-agent-loop-overflow.test.ts +48 -29
  11. package/src/__tests__/conversation-agent-loop.test.ts +48 -96
  12. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  13. package/src/__tests__/conversation-provider-retry-repair.test.ts +52 -49
  14. package/src/__tests__/memory-retrieval-hook.test.ts +68 -30
  15. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  16. package/src/acp/__tests__/helpers/exec-file-stub.ts +12 -7
  17. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -40
  18. package/src/acp/__tests__/session-manager-resume.test.ts +179 -27
  19. package/src/acp/auto-install.test.ts +130 -46
  20. package/src/acp/auto-install.ts +86 -31
  21. package/src/acp/prepare-agent-env.ts +8 -9
  22. package/src/acp/resolve-agent.test.ts +26 -123
  23. package/src/acp/resolve-agent.ts +26 -85
  24. package/src/acp/resume-hint.ts +1 -3
  25. package/src/acp/session-manager.ts +32 -20
  26. package/src/acp/types.ts +0 -8
  27. package/src/agent/loop.ts +42 -26
  28. package/src/config/bundled-skills/acp/SKILL.md +14 -14
  29. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  30. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  31. package/src/config/bundled-skills/app-builder/TOOLS.json +43 -2
  32. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  33. package/src/config/bundled-tool-registry.ts +2 -0
  34. package/src/config/call-site-defaults.ts +0 -2
  35. package/src/config/feature-flag-registry.json +1 -1
  36. package/src/config/schemas/call-site-catalog.ts +0 -14
  37. package/src/config/schemas/llm.ts +0 -2
  38. package/src/context/compactor.ts +22 -4
  39. package/src/daemon/conversation-agent-loop.ts +15 -57
  40. package/src/daemon/conversation.ts +23 -3
  41. package/src/daemon/disk-pressure-policy.ts +0 -1
  42. package/src/daemon/lifecycle.ts +0 -7
  43. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  44. package/src/plugins/defaults/compaction/manager-store.ts +13 -0
  45. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +6 -23
  46. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +24 -25
  47. package/src/runtime/routes/__tests__/acp-routes.test.ts +71 -39
  48. package/src/runtime/routes/__tests__/plugins-routes.test.ts +29 -4
  49. package/src/runtime/routes/plugins-routes.ts +10 -9
  50. package/src/tools/acp/list-agents.test.ts +2 -2
  51. package/src/tools/acp/spawn.test.ts +103 -225
  52. package/src/tools/acp/spawn.ts +10 -120
  53. package/src/tools/apps/executors.ts +153 -42
  54. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  55. package/src/proactive-artifact/decision.test.ts +0 -226
  56. package/src/proactive-artifact/decision.ts +0 -165
  57. package/src/proactive-artifact/index.ts +0 -7
  58. package/src/proactive-artifact/job.test.ts +0 -962
  59. package/src/proactive-artifact/job.ts +0 -372
  60. package/src/proactive-artifact/message-copy.ts +0 -58
  61. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  62. 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
- "default-compaction: contextWindowManager run option is missing — orchestrator must supply it before invoking compaction",
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
- // Strip image contentBlocks from older tool results to prevent
1119
- // screenshots from accumulating in the context window. The LLM
1120
- // already saw each image on the turn it was captured; keeping
1121
- // base64 blobs in history rapidly exhausts the context budget.
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 run on demand via bun, or installed automatically (see below).
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 runs the adapter through bun instead (`bun x --bun <package>`). Bun fetches the package into its cache on first use, so there is no global install and it works on hosts without node or npm (platform-hosted assistants ship bun only). When bun itself is not on PATH, the assistant falls back to silently installing the matching npm package globally and proceeds in the same call.
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 run or installed this way (`@agentclientprotocol/claude-agent-acp`, `@zed-industries/codex-acp`, `@google/gemini-cli`); user-configured agents with custom commands are never fetched or installed automatically.
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 and npm both unavailable, restricted global installs, or an auto-install failure (the failure reason is surfaced in the tool result).
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
- npm i -g @agentclientprotocol/claude-agent-acp # claude
58
- npm i -g @zed-industries/codex-acp # codex
59
- npm i -g @google/gemini-cli # gemini
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 fetched automatically when missing, but it shells out to the underlying `codex` CLI, which must also be on PATH:
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 fetched from `@google/gemini-cli` when missing.
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
- This applies only to adapters installed globally via npm; adapters run via bun are fetched on first use and have no global install to update. If `acp_spawn` reports that an adapter is outdated, ask the user before updating. To update:
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
- npm i -g @agentclientprotocol/claude-agent-acp@latest
126
+ bun add -g @agentclientprotocol/claude-agent-acp@latest
127
127
  # or
128
- npm i -g @zed-industries/codex-acp@latest
128
+ bun add -g @zed-industries/codex-acp@latest
129
129
  # or
130
- npm i -g @google/gemini-cli@latest
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 is runnable (its binary is on PATH, or the assistant can run it via bun), 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?"
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 runs it via bun (fetching the package on first use, no global install) or falls back to an npm auto-install, proceeding in the same call; if neither works, 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.",
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 is runnable (its binary is on PATH, or the assistant can run it via bun), and an install command if not. Use this to decide between 'claude', 'codex', and 'gemini' or to surface setup steps to the user.",
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` (required), `description`, `schema_json`, `source_files`, `preview`, `auto_open`, `change_summary`.
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
- - **Rename / metadata** — edit `/workspace/data/apps/<slug>.json` directly. Not a new app.
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": "Name of the app"
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": ["name"]
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": "assistant",
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",
@@ -77,8 +77,6 @@ export const LLMCallSiteEnum = z.enum([
77
77
  "meetChatOpportunity",
78
78
  "inference",
79
79
  "trustRuleSuggestion",
80
- "proactiveArtifactDecision",
81
- "proactiveArtifactBuild",
82
80
  "homeGreeting",
83
81
  "homeSuggestedPrompts",
84
82
  ]);
@@ -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
- // Append instruction at the tail — prefix unchanged, so prefix cache
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 = [...prefix, instruction];
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);