@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.3
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/docs/activation-funnel-telemetry.md +310 -0
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/feature-flag-registry.json +3 -18
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +78 -78
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +12 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +25 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
* Like the first-party plugin listing, the manifest is fetched from the repo
|
|
13
13
|
* at a git ref (via the GitHub Contents API) rather than bundled into the
|
|
14
14
|
* assistant build — so the whitelist can grow without shipping a new release.
|
|
15
|
-
* Every external source pins an explicit `ref`
|
|
16
|
-
*
|
|
15
|
+
* Every external source pins an explicit `ref` that MUST be a full commit SHA:
|
|
16
|
+
* the fetched code is locked to an immutable revision. Tags and branches are
|
|
17
|
+
* rejected because they are mutable — an upstream owner could retag/repoint
|
|
18
|
+
* them to attacker code that the daemon would then `import()` (the install
|
|
19
|
+
* tree is dynamically loaded). A SHA cannot be repointed, so the reviewed
|
|
20
|
+
* manifest fully determines what gets executed.
|
|
17
21
|
*
|
|
18
22
|
* Designed for direct programmatic use with an injected `fetch`, mirroring
|
|
19
23
|
* {@link ./search-plugins} and {@link ./install-from-github}.
|
|
@@ -39,6 +43,13 @@ const MARKETPLACE_FILE_PATH = "experimental/plugins/marketplace.json";
|
|
|
39
43
|
const REPO_SLUG_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
40
44
|
/** Install name: a single kebab-case path segment (same rule as the CLI). */
|
|
41
45
|
const PLUGIN_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
46
|
+
/**
|
|
47
|
+
* Full Git commit SHA — 40 hex chars (SHA-1) or 64 (SHA-256). External
|
|
48
|
+
* marketplace refs must be a complete object name so the install is pinned to
|
|
49
|
+
* an immutable revision; abbreviated SHAs, tags, and branches are all mutable
|
|
50
|
+
* or ambiguous and are rejected.
|
|
51
|
+
*/
|
|
52
|
+
const COMMIT_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
42
53
|
|
|
43
54
|
const githubSourceSchema = z.object({
|
|
44
55
|
/** Discriminator. Only GitHub sources are resolved today. */
|
|
@@ -57,10 +68,17 @@ const githubSourceSchema = z.object({
|
|
|
57
68
|
)
|
|
58
69
|
.optional(),
|
|
59
70
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
71
|
+
* Immutable revision to fetch the plugin from. Must be a full commit SHA:
|
|
72
|
+
* tags and branches are mutable, so allowing them would let an upstream
|
|
73
|
+
* owner retarget a curated entry at code the daemon later `import()`s (RCE).
|
|
74
|
+
* A full SHA pins the install to exactly the reviewed bytes.
|
|
62
75
|
*/
|
|
63
|
-
ref: z
|
|
76
|
+
ref: z
|
|
77
|
+
.string()
|
|
78
|
+
.regex(
|
|
79
|
+
COMMIT_SHA_RE,
|
|
80
|
+
"expected a full commit SHA (40 or 64 hex chars); tags and branches are mutable and not allowed",
|
|
81
|
+
),
|
|
64
82
|
});
|
|
65
83
|
|
|
66
84
|
const marketplaceEntrySchema = z.object({
|
|
@@ -200,8 +200,22 @@ export async function loadPluginCatalog(
|
|
|
200
200
|
|
|
201
201
|
const matches: PluginSearchMatch[] = [];
|
|
202
202
|
const seen = new Set<string>();
|
|
203
|
+
|
|
204
|
+
// Whitelisted external entries own their name. A same-named
|
|
205
|
+
// `experimental/plugins/<name>/` directory is that plugin's curated adapter
|
|
206
|
+
// stub (overlaid onto the clone at install time), not a standalone
|
|
207
|
+
// first-party plugin — so it must surface once, as the external entry.
|
|
208
|
+
for (const entry of marketplace) {
|
|
209
|
+
if (seen.has(entry.name)) continue;
|
|
210
|
+
matches.push(marketplaceMatch(entry));
|
|
211
|
+
seen.add(entry.name);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// First-party plugins are the in-repo directories whose name the marketplace
|
|
215
|
+
// does not claim.
|
|
203
216
|
for (const entry of entries) {
|
|
204
217
|
if (entry.type !== "dir") continue;
|
|
218
|
+
if (seen.has(entry.name)) continue;
|
|
205
219
|
matches.push({
|
|
206
220
|
name: entry.name,
|
|
207
221
|
path: entry.path,
|
|
@@ -210,14 +224,6 @@ export async function loadPluginCatalog(
|
|
|
210
224
|
seen.add(entry.name);
|
|
211
225
|
}
|
|
212
226
|
|
|
213
|
-
for (const entry of marketplace) {
|
|
214
|
-
// First-party plugins win a name collision — the curated manifest is
|
|
215
|
-
// additive, never an override of what ships in-repo.
|
|
216
|
-
if (seen.has(entry.name)) continue;
|
|
217
|
-
matches.push(marketplaceMatch(entry));
|
|
218
|
-
seen.add(entry.name);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
227
|
matches.sort((a, b) => a.name.localeCompare(b.name));
|
|
222
228
|
|
|
223
229
|
return { ref, matches };
|
|
@@ -9,9 +9,9 @@ const FROZEN_EMPTY_ARGS = Object.freeze([] as string[]) as unknown as string[];
|
|
|
9
9
|
/**
|
|
10
10
|
* Default ACP agent profiles that ship with the assistant.
|
|
11
11
|
*
|
|
12
|
-
* When
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* When the user has not provided a config entry for an agent id, the resolver
|
|
13
|
+
* falls back to this map so common agents like `claude` and `codex` Just Work
|
|
14
|
+
* without requiring per-user config.
|
|
15
15
|
*
|
|
16
16
|
* Keyed by agent id. Deeply frozen — the outer object, each profile, and the
|
|
17
17
|
* `args` arrays — so mutation throws in strict mode rather than silently
|
package/src/config/acp-schema.ts
CHANGED
|
@@ -20,12 +20,6 @@ const AcpAgentConfigSchema = z
|
|
|
20
20
|
|
|
21
21
|
export const AcpConfigSchema = z
|
|
22
22
|
.object({
|
|
23
|
-
enabled: z
|
|
24
|
-
.boolean()
|
|
25
|
-
.default(false)
|
|
26
|
-
.describe(
|
|
27
|
-
"Whether the Agent Communication Protocol (ACP) system is enabled",
|
|
28
|
-
),
|
|
29
23
|
maxConcurrentSessions: z
|
|
30
24
|
.number()
|
|
31
25
|
.int()
|
|
@@ -40,7 +34,7 @@ export const AcpConfigSchema = z
|
|
|
40
34
|
.describe("Map of agent names to their configurations"),
|
|
41
35
|
})
|
|
42
36
|
.describe(
|
|
43
|
-
"Agent Communication Protocol (ACP) —
|
|
37
|
+
"Agent Communication Protocol (ACP) — inter-agent communication and delegation",
|
|
44
38
|
);
|
|
45
39
|
|
|
46
40
|
export type AcpConfig = z.infer<typeof AcpConfigSchema>;
|
|
@@ -26,24 +26,11 @@ Users can refer to agents by natural names: "claude code", "codex cli", "openai
|
|
|
26
26
|
|
|
27
27
|
## First-time setup
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
ACP is always available - default profiles for `claude`, `codex`, and `gemini` ship out-of-box, so no config edit is needed to start. First-time setup is just making the adapter binary available, then spawning:
|
|
30
30
|
|
|
31
|
-
1.
|
|
31
|
+
1. Install the adapter binary if it's missing. This happens automatically: when `acp_spawn` finds the agent's binary missing from PATH, the assistant installs it once via a sandboxed bun global install and proceeds in the same call (see "Automatic adapter availability" below).
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{
|
|
36
|
-
"acp": {
|
|
37
|
-
"enabled": true,
|
|
38
|
-
"maxConcurrentSessions": 4
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
If you go the config route, **wait a few seconds** for the config watcher to pick up the change (it hot-reloads automatically - no restart needed).
|
|
43
|
-
|
|
44
|
-
2. Then retry the `acp_spawn` call. Do NOT run `vellum sleep && vellum wake` - that kills the conversation.
|
|
45
|
-
|
|
46
|
-
No manual binary installation is needed first: missing adapter binaries are installed automatically (see below).
|
|
33
|
+
2. Call `acp_spawn`. Do NOT run `vellum sleep && vellum wake` - that kills the conversation.
|
|
47
34
|
|
|
48
35
|
## Automatic adapter availability
|
|
49
36
|
|
|
@@ -140,7 +127,7 @@ Then retry the `acp_spawn` call.
|
|
|
140
127
|
|
|
141
128
|
## Discoverability
|
|
142
129
|
|
|
143
|
-
Use `acp_list_agents` to see what's set up and what's missing. It returns each available agent profile, whether
|
|
130
|
+
Use `acp_list_agents` to see what's set up and what's missing. It returns each available agent profile, 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
131
|
|
|
145
132
|
## Working directory
|
|
146
133
|
|
|
@@ -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 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.
|
|
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.",
|
|
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
|
|
90
|
+
"description": "Lists ACP coding agents available to spawn. Each entry includes 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": {
|
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
"scope": "client",
|
|
40
40
|
"key": "experiment-activation-flow-2026-06-03",
|
|
41
41
|
"label": "Activation Flow Experiment 2026-06-03",
|
|
42
|
-
"description": "
|
|
43
|
-
"defaultEnabled":
|
|
42
|
+
"description": "Multivariate activation-flow experiment. control = standard flow; variant-a = activation rail. Targeted via LaunchDarkly.",
|
|
43
|
+
"defaultEnabled": "control",
|
|
44
|
+
"values": ["control", "variant-a"]
|
|
44
45
|
},
|
|
45
46
|
{
|
|
46
47
|
"id": "local-docker-enabled",
|
|
@@ -481,22 +482,6 @@
|
|
|
481
482
|
"label": "Self-intro first message",
|
|
482
483
|
"description": "On the first conversation, send a natural self-introduction (e.g. \"Hi Vela, I'm alex. Nice to meet you.\") on the user's behalf and route it through real LLM inference, instead of serving the canned first greeting. Names come from the onboarding context; falls back to the canned greeting when no name is known. Exposed to clients so pre-hatch onboarding can compute the source-of-truth initial message, and to the assistant so older clients can still be gated server-side. See assistant/src/daemon/first-greeting.ts (buildSelfIntroMessage).",
|
|
483
484
|
"defaultEnabled": false
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
"id": "provider-first-profile-creation",
|
|
487
|
-
"scope": "client",
|
|
488
|
-
"key": "provider-first-profile-creation",
|
|
489
|
-
"label": "Provider-First Profile Creation",
|
|
490
|
-
"description": "New profile creation flow: pick (or inline-create) a provider first, then a model, with pre-filled provider and profile name/key, plus a quick-add \"+\" in the chat composer's Model Profile menu. When off, profile creation uses the previous field order and there is no composer quick-add.",
|
|
491
|
-
"defaultEnabled": false
|
|
492
|
-
},
|
|
493
|
-
{
|
|
494
|
-
"id": "acp",
|
|
495
|
-
"scope": "assistant",
|
|
496
|
-
"key": "acp",
|
|
497
|
-
"label": "ACP Coding Agents",
|
|
498
|
-
"description": "Enable spawning and steering external coding agents (Claude Code, Codex, Gemini) via the Agent Client Protocol. Alternative gate to the acp.enabled workspace config field; either enables the subsystem.",
|
|
499
|
-
"defaultEnabled": false
|
|
500
485
|
}
|
|
501
486
|
]
|
|
502
487
|
}
|
|
@@ -22,6 +22,34 @@ export const TOOL_RESULT_DIR = ".tool-results";
|
|
|
22
22
|
/** Marker used to detect already-truncated results (idempotency guard). */
|
|
23
23
|
export const TRUNCATION_MARKER = "\u2014 full result:";
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Tools whose results carry durable operating instructions the model relies on
|
|
27
|
+
* across later turns rather than one-off data it only needs in the moment.
|
|
28
|
+
* Their results must never be middle-truncated: paging out the middle silently
|
|
29
|
+
* strips the workflow (e.g. a `skill_load` body losing its "## Available Tools"
|
|
30
|
+
* section), leaving the model to fall back to generic priors. Skill bodies are
|
|
31
|
+
* bounded and authored to live in context, so exempting them is safe.
|
|
32
|
+
*/
|
|
33
|
+
export const TRUNCATION_EXEMPT_TOOLS = new Set<string>(["skill_load"]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a map of tool_use_id -> originating tool name by walking the tool_use
|
|
37
|
+
* blocks in assistant messages. A tool_result only carries `tool_use_id`, so
|
|
38
|
+
* this is the only way to recover which tool produced a given result.
|
|
39
|
+
*/
|
|
40
|
+
function buildToolNameById(messages: Message[]): Map<string, string> {
|
|
41
|
+
const byId = new Map<string, string>();
|
|
42
|
+
for (const msg of messages) {
|
|
43
|
+
if (msg.role !== "assistant") continue;
|
|
44
|
+
for (const block of msg.content) {
|
|
45
|
+
if (block.type !== "tool_use") continue;
|
|
46
|
+
const tu = block as ToolUseContent;
|
|
47
|
+
byId.set(tu.id, tu.name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return byId;
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
/**
|
|
26
54
|
* Deterministic file path for a tool result's full content on disk.
|
|
27
55
|
* Uses the first 12 hex chars of the SHA-256 of the tool_use_id.
|
|
@@ -59,7 +87,8 @@ export function buildTruncatedContent(
|
|
|
59
87
|
* - The in-context content is replaced with a prefix/suffix stub.
|
|
60
88
|
*
|
|
61
89
|
* Results are skipped if they are below threshold, are error results,
|
|
62
|
-
*
|
|
90
|
+
* have already been truncated (contain `TRUNCATION_MARKER`), or were produced
|
|
91
|
+
* by a tool in `TRUNCATION_EXEMPT_TOOLS` (durable instructions like skill bodies).
|
|
63
92
|
*
|
|
64
93
|
* Returns a shallow-copied messages array (only modified messages are cloned)
|
|
65
94
|
* and the count of results that were truncated.
|
|
@@ -70,6 +99,8 @@ export function postTurnTruncateToolResults(
|
|
|
70
99
|
): { messages: Message[]; truncatedCount: number } {
|
|
71
100
|
let truncatedCount = 0;
|
|
72
101
|
|
|
102
|
+
const toolNameById = buildToolNameById(messages);
|
|
103
|
+
|
|
73
104
|
const mapped = messages.map((msg) => {
|
|
74
105
|
let changed = false;
|
|
75
106
|
const nextContent: ContentBlock[] = msg.content.map((block) => {
|
|
@@ -82,6 +113,13 @@ export function postTurnTruncateToolResults(
|
|
|
82
113
|
// Skip error results.
|
|
83
114
|
if (tr.is_error) return block;
|
|
84
115
|
|
|
116
|
+
// Skip results from tools whose output is durable operating instructions
|
|
117
|
+
// (e.g. skill_load); middle-truncating them strips the workflow.
|
|
118
|
+
const toolName = toolNameById.get(tr.tool_use_id);
|
|
119
|
+
if (toolName !== undefined && TRUNCATION_EXEMPT_TOOLS.has(toolName)) {
|
|
120
|
+
return block;
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
// Skip already-truncated results (idempotency).
|
|
86
124
|
if (tr.content.includes(TRUNCATION_MARKER)) return block;
|
|
87
125
|
|
|
@@ -17,7 +17,6 @@ import type {
|
|
|
17
17
|
import { getConfig } from "../config/loader.js";
|
|
18
18
|
import { recordEstimate } from "../context/estimator-calibration.js";
|
|
19
19
|
import { getCalibrationProviderKey } from "../context/token-estimator.js";
|
|
20
|
-
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
21
20
|
import { projectAssistantMessage } from "../memory/conversation-attention-store.js";
|
|
22
21
|
import {
|
|
23
22
|
deleteMessageById,
|
|
@@ -46,6 +45,7 @@ import {
|
|
|
46
45
|
type SlackMessageMetadata,
|
|
47
46
|
writeSlackMetadata,
|
|
48
47
|
} from "../messaging/providers/slack/message-metadata.js";
|
|
48
|
+
import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
|
|
49
49
|
import type {
|
|
50
50
|
ContentBlock,
|
|
51
51
|
ImageContent,
|
|
@@ -1482,6 +1482,13 @@ function annotatePersistedAssistantMessage(
|
|
|
1482
1482
|
display: surface.display,
|
|
1483
1483
|
...(surface.persistent ? { persistent: true } : {}),
|
|
1484
1484
|
...(surface.toolCallId ? { toolCallId: surface.toolCallId } : {}),
|
|
1485
|
+
// Daemon-only commit-timing activation tag, persisted so
|
|
1486
|
+
// restoreSurfaceStateFromHistory can rehydrate it after a reload. This
|
|
1487
|
+
// block lives only in server-side conversation history, never in the
|
|
1488
|
+
// client `ui_surface_show` message.
|
|
1489
|
+
...(surface.activationMoment
|
|
1490
|
+
? { activationMoment: surface.activationMoment }
|
|
1491
|
+
: {}),
|
|
1485
1492
|
} as unknown as ContentBlock);
|
|
1486
1493
|
}
|
|
1487
1494
|
modified = true;
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
import { v4 as uuid } from "uuid";
|
|
11
11
|
|
|
12
|
-
import { optimizeImageForTransport } from "../agent/image-optimize.js";
|
|
13
12
|
import type {
|
|
14
13
|
AgentEvent,
|
|
15
14
|
AgentLoopExitReason,
|
|
@@ -43,7 +42,6 @@ import {
|
|
|
43
42
|
estimatePromptTokens,
|
|
44
43
|
getCalibrationProviderKey,
|
|
45
44
|
} from "../context/token-estimator.js";
|
|
46
|
-
import type { ContextWindowCompactOptions } from "../context/window-manager.js";
|
|
47
45
|
import { writeRelationshipState } from "../home/relationship-state-writer.js";
|
|
48
46
|
import {
|
|
49
47
|
clearSentryConversationContext,
|
|
@@ -81,6 +79,7 @@ import {
|
|
|
81
79
|
reduceContextOverflow,
|
|
82
80
|
type ReducerState,
|
|
83
81
|
} from "../plugins/defaults/compaction/context-overflow-reducer.js";
|
|
82
|
+
import type { ContextWindowCompactOptions } from "../plugins/defaults/compaction/window-manager.js";
|
|
84
83
|
import { deepRepairHistory } from "../plugins/defaults/history-repair/terminal.js";
|
|
85
84
|
import userPromptSubmitMemoryRetrieval, {
|
|
86
85
|
type MemoryRetrievalHookContext,
|
|
@@ -88,13 +87,11 @@ import userPromptSubmitMemoryRetrieval, {
|
|
|
88
87
|
import { runHook } from "../plugins/pipeline.js";
|
|
89
88
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
90
89
|
import type { Provider } from "../providers/types.js";
|
|
91
|
-
import {
|
|
92
|
-
isUntrustedTrustClass,
|
|
93
|
-
resolveActorTrust,
|
|
94
|
-
} from "../runtime/actor-trust-resolver.js";
|
|
90
|
+
import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
|
|
95
91
|
import { broadcastMessage } from "../runtime/assistant-event-hub.js";
|
|
96
92
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
97
93
|
import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
|
|
94
|
+
import type { ActivationMomentParam } from "../telemetry/activation-funnel.js";
|
|
98
95
|
import type { UsageActor } from "../usage/actors.js";
|
|
99
96
|
import { getLogger } from "../util/logger.js";
|
|
100
97
|
import { timeAgo } from "../util/time.js";
|
|
@@ -122,16 +119,12 @@ import {
|
|
|
122
119
|
isUserCancellation,
|
|
123
120
|
} from "./conversation-error.js";
|
|
124
121
|
import { raceWithTimeout } from "./conversation-media-retry.js";
|
|
125
|
-
import type {
|
|
126
|
-
InboundActorContext,
|
|
127
|
-
InjectionMode,
|
|
128
|
-
} from "./conversation-runtime-assembly.js";
|
|
122
|
+
import type { InjectionMode } from "./conversation-runtime-assembly.js";
|
|
129
123
|
import {
|
|
130
124
|
applyRuntimeInjections,
|
|
131
125
|
getSlackCompactionWatermarkForPrefix,
|
|
132
|
-
inboundActorContextFromTrust,
|
|
133
|
-
inboundActorContextFromTrustContext,
|
|
134
126
|
loadSlackChronologicalContext,
|
|
127
|
+
resolveTurnInboundActorContext,
|
|
135
128
|
type SlackChronologicalContext,
|
|
136
129
|
stripInjectionsForCompaction,
|
|
137
130
|
} from "./conversation-runtime-assembly.js";
|
|
@@ -148,8 +141,8 @@ import type {
|
|
|
148
141
|
} from "./message-protocol.js";
|
|
149
142
|
import { parseActualTokensFromError } from "./parse-actual-tokens-from-error.js";
|
|
150
143
|
import {
|
|
144
|
+
oversizedImageReplacement,
|
|
151
145
|
persistUnsendableImageDowngrades,
|
|
152
|
-
UNSENDABLE_IMAGE_NOTE,
|
|
153
146
|
} from "./persist-unsendable-image.js";
|
|
154
147
|
import { resolveTrustClass, type TrustContext } from "./trust-context.js";
|
|
155
148
|
|
|
@@ -176,6 +169,34 @@ function formatDiskPressureBlockedMessage(): string {
|
|
|
176
169
|
return "Storage is critically low, so background processes are paused and remote messages are ignored until the guardian frees enough space. Remote senders should try again later.";
|
|
177
170
|
}
|
|
178
171
|
|
|
172
|
+
// ── Image-recovery helpers ───────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* True when a message's content holds an image the provider may have rejected
|
|
176
|
+
* for being oversized — either a top-level image block (user upload) or one
|
|
177
|
+
* nested inside a tool_result's contentBlocks (e.g. a browser screenshot).
|
|
178
|
+
*/
|
|
179
|
+
function messageHasImageBlock(content: ContentBlock[]): boolean {
|
|
180
|
+
return content.some(
|
|
181
|
+
(b) =>
|
|
182
|
+
b.type === "image" ||
|
|
183
|
+
(b.type === "tool_result" &&
|
|
184
|
+
(b.contentBlocks?.some((cb) => cb.type === "image") ?? false)),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Replace an oversized image with its downscaled form or an unsendable note,
|
|
190
|
+
* leaving still-sendable images untouched. Delegates to the shared
|
|
191
|
+
* {@link oversizedImageReplacement} so the in-memory recovery and the durable
|
|
192
|
+
* persist pass apply the identical provider-cap gate.
|
|
193
|
+
*/
|
|
194
|
+
function recoverImageBlock(
|
|
195
|
+
block: Extract<ContentBlock, { type: "image" }>,
|
|
196
|
+
): ContentBlock {
|
|
197
|
+
return oversizedImageReplacement(block) ?? block;
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
// ── Plugin pipeline helpers ──────────────────────────────────────────
|
|
180
201
|
|
|
181
202
|
/**
|
|
@@ -223,6 +244,14 @@ export interface AssistantSurface {
|
|
|
223
244
|
persistent?: boolean;
|
|
224
245
|
/** Id of the tool call that produced this surface (the `ui_show` proxy tool). Persisted so app previews can gate on the tool result's arrival rather than whole-turn streaming state. */
|
|
225
246
|
toolCallId?: string;
|
|
247
|
+
/**
|
|
248
|
+
* Commit-timing activation-rail tag (daemon-only). Persisted into the
|
|
249
|
+
* server-side `ui_surface` history block — NOT the client `ui_surface_show`
|
|
250
|
+
* message — so `restoreSurfaceStateFromHistory` can rehydrate the tag and a
|
|
251
|
+
* post-reload commit still records its funnel milestone. Show-timing moments
|
|
252
|
+
* record at render and are never stored here.
|
|
253
|
+
*/
|
|
254
|
+
activationMoment?: ActivationMomentParam;
|
|
226
255
|
}
|
|
227
256
|
|
|
228
257
|
// ── runAgentLoop ─────────────────────────────────────────────────────
|
|
@@ -771,41 +800,16 @@ export async function runAgentLoopImpl(
|
|
|
771
800
|
hostTimeZone,
|
|
772
801
|
});
|
|
773
802
|
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
assistantId: ctx.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
785
|
-
sourceChannel: gc.sourceChannel,
|
|
786
|
-
conversationExternalId: gc.requesterChatId,
|
|
787
|
-
actorExternalId: gc.requesterExternalUserId,
|
|
788
|
-
actorDisplayName: gc.requesterSenderDisplayName,
|
|
789
|
-
});
|
|
790
|
-
resolvedInboundActorContext = inboundActorContextFromTrust(actorTrust);
|
|
791
|
-
} else {
|
|
792
|
-
resolvedInboundActorContext = inboundActorContextFromTrustContext(gc);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Resolve the guardian flag for this turn. It derives only from the
|
|
797
|
-
// resolved actor trust class — never from retrieval — so it settles before
|
|
798
|
-
// context assembly.
|
|
799
|
-
const isGuardian =
|
|
800
|
-
resolvedInboundActorContext?.trustClass === "guardian" ||
|
|
801
|
-
!resolvedInboundActorContext;
|
|
802
|
-
|
|
803
|
-
// Unified `<turn_context>` actor input, included only for non-guardian
|
|
804
|
-
// turns. Resolved once at turn start and threaded per call site (like
|
|
805
|
-
// `modelProfile`) so post-compaction re-injection receives it as an
|
|
806
|
-
// explicit hook input rather than re-deriving it from live state that can
|
|
807
|
-
// flip mid-turn.
|
|
808
|
-
const actorContext = isGuardian ? null : resolvedInboundActorContext;
|
|
803
|
+
// Unified `<turn_context>` actor input for this turn (model-facing grounding
|
|
804
|
+
// metadata; the conversation runtime context remains the source for policy
|
|
805
|
+
// gating). Resolved once at turn start and threaded per call site (like
|
|
806
|
+
// `modelProfile`) so post-compaction re-injection receives it as an explicit
|
|
807
|
+
// hook input rather than re-deriving it from live state that can flip
|
|
808
|
+
// mid-turn.
|
|
809
|
+
const actorContext = resolveTurnInboundActorContext(
|
|
810
|
+
ctx.trustContext,
|
|
811
|
+
ctx.assistantId,
|
|
812
|
+
);
|
|
809
813
|
|
|
810
814
|
// Surface long gaps between user messages so the model can acknowledge
|
|
811
815
|
// the absence naturally. Gated at >12h to avoid noisy injection during
|
|
@@ -873,10 +877,9 @@ export async function runAgentLoopImpl(
|
|
|
873
877
|
// `user-prompt-submit-temp` hook handler but invoked directly for now,
|
|
874
878
|
// separate from the canonical late `user-prompt-submit` hook (history
|
|
875
879
|
// repair, title) that fires just before the loop.
|
|
876
|
-
// The injection inputs (`
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
// state that can flip mid-turn.
|
|
880
|
+
// The injection inputs (`isNonInteractive`, `modelProfile`) are resolved
|
|
881
|
+
// once at turn start and threaded in so post-compaction re-injection reuses
|
|
882
|
+
// the same snapshot rather than live state that can flip mid-turn.
|
|
880
883
|
const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
|
|
881
884
|
let currentInjectionMode: InjectionMode = "full";
|
|
882
885
|
const memoryCtx: MemoryRetrievalHookContext = {
|
|
@@ -886,10 +889,8 @@ export async function runAgentLoopImpl(
|
|
|
886
889
|
logger: rlog,
|
|
887
890
|
latestMessages: ctx.messages,
|
|
888
891
|
requestId: reqId,
|
|
889
|
-
mode: currentInjectionMode,
|
|
890
892
|
isNonInteractive,
|
|
891
893
|
modelProfile: modelProfileStr,
|
|
892
|
-
actorContext,
|
|
893
894
|
};
|
|
894
895
|
await userPromptSubmitMemoryRetrieval(memoryCtx);
|
|
895
896
|
|
|
@@ -913,6 +914,8 @@ export async function runAgentLoopImpl(
|
|
|
913
914
|
// `AgentLoopRunResult.newMessages`, which is what persistence consumes.
|
|
914
915
|
const userPromptCtx: UserPromptSubmitContext = {
|
|
915
916
|
conversationId: ctx.conversationId,
|
|
917
|
+
userMessageId,
|
|
918
|
+
requestId: reqId,
|
|
916
919
|
prompt: options?.titleText ?? content,
|
|
917
920
|
originalMessages: ctx.messages,
|
|
918
921
|
latestMessages: runMessages,
|
|
@@ -1076,44 +1079,41 @@ export async function runAgentLoopImpl(
|
|
|
1076
1079
|
|
|
1077
1080
|
// ── Image-dimension overflow recovery ──────────────────────────
|
|
1078
1081
|
// When the provider rejects because an image block exceeds its pixel
|
|
1079
|
-
// cap,
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1082
|
+
// or payload cap, recover every oversized image in ctx.messages and
|
|
1083
|
+
// retry once. recoverImageBlock downscales an oversized image, or swaps
|
|
1084
|
+
// it for a text note when resize is a no-op (e.g. sips unavailable
|
|
1085
|
+
// off macOS), while leaving still-sendable images untouched. This covers
|
|
1086
|
+
// both top-level image blocks (user uploads) and images nested inside a
|
|
1087
|
+
// tool_result's contentBlocks (e.g. a browser screenshot), which is where
|
|
1088
|
+
// the rejected block usually lives.
|
|
1084
1089
|
if (state.imageTooLargeDetected) {
|
|
1085
1090
|
state.imageTooLargeDetected = false;
|
|
1086
1091
|
rlog.warn(
|
|
1087
1092
|
{ phase: "image-recovery" },
|
|
1088
|
-
"Image too large —
|
|
1093
|
+
"Image too large — recovering oversized image blocks and retrying",
|
|
1089
1094
|
);
|
|
1090
1095
|
ctx.messages = ctx.messages.map((msg) => {
|
|
1091
1096
|
if (!Array.isArray(msg.content)) return msg;
|
|
1092
|
-
if (!msg.content
|
|
1097
|
+
if (!messageHasImageBlock(msg.content)) return msg;
|
|
1093
1098
|
return {
|
|
1094
1099
|
...msg,
|
|
1095
1100
|
content: msg.content.flatMap((b): ContentBlock[] => {
|
|
1096
|
-
if (b.type
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
if (
|
|
1102
|
-
// sips managed to downscale — use the smaller version
|
|
1101
|
+
if (b.type === "image") return [recoverImageBlock(b)];
|
|
1102
|
+
// Images returned by a tool (e.g. browser_screenshot) live in
|
|
1103
|
+
// the tool_result's contentBlocks, not as top-level blocks.
|
|
1104
|
+
// Recover them in place so the tool_use/tool_result pairing
|
|
1105
|
+
// stays intact rather than dropping the whole tool_result.
|
|
1106
|
+
if (b.type === "tool_result" && b.contentBlocks?.length) {
|
|
1103
1107
|
return [
|
|
1104
1108
|
{
|
|
1105
1109
|
...b,
|
|
1106
|
-
|
|
1107
|
-
type
|
|
1108
|
-
|
|
1109
|
-
data: resized.data,
|
|
1110
|
-
},
|
|
1110
|
+
contentBlocks: b.contentBlocks.map((cb) =>
|
|
1111
|
+
cb.type === "image" ? recoverImageBlock(cb) : cb,
|
|
1112
|
+
),
|
|
1111
1113
|
},
|
|
1112
1114
|
];
|
|
1113
1115
|
}
|
|
1114
|
-
|
|
1115
|
-
// can explain the situation rather than silently dropping context
|
|
1116
|
-
return [{ type: "text" as const, text: UNSENDABLE_IMAGE_NOTE }];
|
|
1116
|
+
return [b];
|
|
1117
1117
|
}),
|
|
1118
1118
|
};
|
|
1119
1119
|
});
|
|
@@ -1302,7 +1302,7 @@ export async function runAgentLoopImpl(
|
|
|
1302
1302
|
reducerState,
|
|
1303
1303
|
(msgs, signal, opts) =>
|
|
1304
1304
|
defaultCompact({
|
|
1305
|
-
|
|
1305
|
+
conversationId: ctx.conversationId,
|
|
1306
1306
|
messages: msgs,
|
|
1307
1307
|
signal,
|
|
1308
1308
|
...((opts ?? {}) as ContextWindowCompactOptions),
|
|
@@ -1399,7 +1399,7 @@ export async function runAgentLoopImpl(
|
|
|
1399
1399
|
requestId: reqId,
|
|
1400
1400
|
});
|
|
1401
1401
|
const emergencyCompact = await defaultCompact({
|
|
1402
|
-
|
|
1402
|
+
conversationId: ctx.conversationId,
|
|
1403
1403
|
messages: ctx.messages,
|
|
1404
1404
|
signal: abortController.signal,
|
|
1405
1405
|
force: true,
|