@inflexa-ai/harness 0.39.2 → 0.40.0
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/README.md +2 -3
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +13 -2
- package/dist/agents/report-session-agent.d.ts +3 -0
- package/dist/agents/report-session-agent.js +2 -0
- package/dist/agents/sandbox/cheminformatics-agent.js +0 -3
- package/dist/agents/sandbox/shared.d.ts +11 -2
- package/dist/agents/sandbox/shared.js +4 -0
- package/dist/app/chat-turn.d.ts +75 -12
- package/dist/app/chat-turn.js +149 -21
- package/dist/app/message-assembly.d.ts +16 -52
- package/dist/app/message-assembly.js +41 -68
- package/dist/app/run-activity.d.ts +4 -6
- package/dist/app/run-activity.js +7 -27
- package/dist/app/spawn-report-session.d.ts +6 -11
- package/dist/app/spawn-report-session.js +5 -6
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +1 -0
- package/dist/billing/usage-recorder.d.ts +3 -4
- package/dist/billing/usage-recorder.js +3 -4
- package/dist/contracts/chat-parts.d.ts +11 -1
- package/dist/contracts/chat-parts.js +1 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/report-blocks.d.ts +8 -8
- package/dist/contracts/schemas/chat-parts.d.ts +32 -9
- package/dist/contracts/schemas/chat-parts.js +10 -0
- package/dist/execution/artifact-metadata.d.ts +26 -33
- package/dist/execution/artifact-metadata.js +44 -106
- package/dist/execution/post-step-pipeline.d.ts +23 -16
- package/dist/execution/post-step-pipeline.js +19 -21
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +11 -1
- package/dist/execution/step-summary.d.ts +21 -30
- package/dist/execution/step-summary.js +30 -57
- package/dist/index.d.ts +9 -8
- package/dist/index.js +15 -34
- package/dist/lib/zod-issues.js +2 -4
- package/dist/loop/compaction.d.ts +20 -0
- package/dist/loop/compaction.js +2 -0
- package/dist/loop/continue-agent.d.ts +40 -0
- package/dist/loop/continue-agent.js +38 -0
- package/dist/loop/metrics.d.ts +30 -22
- package/dist/loop/metrics.js +51 -22
- package/dist/loop/run-agent.d.ts +111 -13
- package/dist/loop/run-agent.js +467 -220
- package/dist/loop/run-to-terminal.d.ts +16 -17
- package/dist/loop/run-to-terminal.js +24 -35
- package/dist/loop/tool-mask.d.ts +23 -0
- package/dist/loop/tool-mask.js +46 -0
- package/dist/loop/tool-output.d.ts +48 -0
- package/dist/loop/tool-output.js +79 -0
- package/dist/loop/types.d.ts +6 -3
- package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
- package/dist/memory/ai-sdk-message-storage.js +78 -2
- package/dist/memory/conversation-display-recorder.d.ts +6 -0
- package/dist/memory/conversation-display-recorder.js +64 -22
- package/dist/memory/conversation-display-replay.d.ts +5 -14
- package/dist/memory/conversation-display-replay.js +66 -32
- package/dist/memory/conversation-display-storage.d.ts +2 -1
- package/dist/memory/conversation-display-storage.js +2 -1
- package/dist/memory/conversation-view.d.ts +53 -0
- package/dist/memory/conversation-view.js +130 -0
- package/dist/memory/count-tokens.d.ts +5 -7
- package/dist/memory/count-tokens.js +7 -9
- package/dist/memory/thread-history.d.ts +74 -102
- package/dist/memory/thread-history.js +181 -246
- package/dist/memory/thread-store.d.ts +3 -3
- package/dist/memory/thread-store.js +7 -2
- package/dist/memory/tool-call-integrity.d.ts +15 -21
- package/dist/memory/tool-call-integrity.js +31 -38
- package/dist/memory/working-memory.d.ts +2 -5
- package/dist/memory/working-memory.js +2 -5
- package/dist/prompts/SOUL.d.ts +2 -2
- package/dist/prompts/SOUL.js +2 -7
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +16 -32
- package/dist/prompts/briefing.js +1 -1
- package/dist/prompts/compaction.d.ts +4 -0
- package/dist/prompts/compaction.js +24 -0
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +26 -35
- package/dist/prompts/execute-analysis/step-summary.js +7 -5
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +29 -34
- package/dist/prompts/planner.js +15 -29
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +2 -33
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +1 -2
- package/dist/prompts/sandbox/data-profiler.js +6 -3
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +3 -2
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +14 -21
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +7 -8
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +3 -4
- package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
- package/dist/prompts/sandbox/scientific-executor.js +7 -10
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +2 -4
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
- package/dist/prompts/sandbox-standards.d.ts +7 -7
- package/dist/prompts/sandbox-standards.js +74 -76
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +16 -42
- package/dist/providers/ai-sdk.d.ts +39 -6
- package/dist/providers/ai-sdk.js +117 -18
- package/dist/providers/errors.d.ts +2 -0
- package/dist/providers/errors.js +5 -0
- package/dist/providers/prompt-cache.d.ts +43 -41
- package/dist/providers/prompt-cache.js +47 -41
- package/dist/providers/reasoning.d.ts +10 -21
- package/dist/providers/reasoning.js +10 -21
- package/dist/providers/types.d.ts +21 -5
- package/dist/report-model/authoring-grammar.d.ts +2 -2
- package/dist/runtime/assemble.d.ts +9 -7
- package/dist/runtime/assemble.js +7 -3
- package/dist/sandbox/create-sandbox.d.ts +2 -2
- package/dist/sandbox/submit-exec.d.ts +1 -1
- package/dist/schemas/artifact-manifest.d.ts +4 -4
- package/dist/state/init.js +39 -4
- package/dist/state/purge-analysis.js +4 -0
- package/dist/state/tool-outputs.d.ts +4 -0
- package/dist/state/tool-outputs.js +30 -0
- package/dist/tasks/data-profile.d.ts +3 -0
- package/dist/tasks/data-profile.js +2 -0
- package/dist/tools/ad-hoc-router.d.ts +8 -0
- package/dist/tools/ad-hoc-router.js +21 -2
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +3 -0
- package/dist/tools/execute-analysis.js +9 -1
- package/dist/tools/read-tool-output.d.ts +7 -0
- package/dist/tools/read-tool-output.js +130 -0
- package/dist/tools/research/generate-analogy-report.d.ts +10 -26
- package/dist/tools/research/generate-analogy-report.js +76 -163
- package/dist/tools/research/generate-plan.d.ts +3 -0
- package/dist/tools/research/generate-plan.js +4 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +3 -0
- package/dist/tools/sandbox/report-blocker.d.ts +18 -5
- package/dist/tools/sandbox/report-blocker.js +54 -6
- package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
- package/dist/tools/sandbox/submit-file-metadata.js +67 -0
- package/dist/tools/start-report-session.d.ts +1 -3
- package/dist/tools/start-report-session.js +0 -2
- package/dist/tools/workspace/execute-command.js +7 -4
- package/dist/tools/workspace/result-bounds.d.ts +4 -4
- package/dist/tools/workspace/result-bounds.js +6 -5
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +1 -0
- package/dist/workflows/sandbox-step.d.ts +30 -9
- package/dist/workflows/sandbox-step.js +37 -8
- package/package.json +9 -9
- package/dist/providers/anthropic.d.ts +0 -25
- package/dist/providers/anthropic.js +0 -28
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
* Some agents communicate their result EXCLUSIVELY through a terminal tool
|
|
5
5
|
* (`submit_plan`, `submit_profile`, `submit_synthesis`, …):
|
|
6
6
|
* the loop's text reply is discarded and the outcome is read from a closure
|
|
7
|
-
* cell the terminal tool writes.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* cell the terminal tool writes. The wrap-up of a capped run lets no tool run,
|
|
8
|
+
* so an agent that burns its whole iteration budget — or simply stops on
|
|
9
|
+
* prose — without ever submitting leaves that cell empty and forces the caller
|
|
10
|
+
* to hard-fail.
|
|
11
11
|
*
|
|
12
12
|
* `runToTerminal` runs the agent, then — if the outcome cell is still empty
|
|
13
|
-
* and the run was not aborted —
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* and the run was not aborted — runs ONE salvage continuation of the same
|
|
14
|
+
* conversation (`continueAgent`), opened by a corrective nudge. The salvage
|
|
15
|
+
* still declares every tool of the agent, masked to the terminal tools only,
|
|
16
|
+
* because the tool set is part of the prefix a signed thinking block and the
|
|
17
|
+
* prompt cache bind to.
|
|
18
18
|
*
|
|
19
19
|
* Salvage steps are namespaced (`salvage:…`) so a durable (DBOS) caller does
|
|
20
20
|
* not collide the continuation's `llm-*` / `tool-*` cache keys with the first
|
|
@@ -50,18 +50,17 @@ export interface RunToTerminalResult extends RunAgentResult {
|
|
|
50
50
|
}
|
|
51
51
|
/** Describes how to salvage a run that never reached its terminal tool. */
|
|
52
52
|
export interface TerminalSalvage {
|
|
53
|
-
/**
|
|
54
|
-
* the same instances the first run used — they close over the outcome cell. */
|
|
53
|
+
/** Their ids become the mask of the salvage continuation, so each must already be a declared tool of the agent. */
|
|
55
54
|
readonly tools: readonly Tool[];
|
|
56
|
-
/**
|
|
55
|
+
/** Opens the salvage continuation as a synthetic user message. */
|
|
57
56
|
readonly nudge: string;
|
|
58
|
-
/**
|
|
57
|
+
/** The cap of salvage requests. Defaults to {@link DEFAULT_SALVAGE_ITERATIONS}. */
|
|
59
58
|
readonly maxIterations?: number;
|
|
60
59
|
}
|
|
61
60
|
/**
|
|
62
|
-
* Drive `agent` to its terminal tool, salvaging once if it
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
61
|
+
* Drive `agent` to its terminal tool, salvaging once if it does not get there.
|
|
62
|
+
* A salvage result carries both runs' messages and summed rollups, with the
|
|
63
|
+
* salvage's own finish. Throws first if a terminal tool is not declared on the
|
|
64
|
+
* agent.
|
|
66
65
|
*/
|
|
67
66
|
export declare function runToTerminal(agent: AgentDefinition, initial: readonly LoopMessage[], session: AgentSession, opts: RunAgentOptions, salvage: TerminalSalvage): Promise<RunToTerminalResult>;
|
|
@@ -4,46 +4,45 @@
|
|
|
4
4
|
* Some agents communicate their result EXCLUSIVELY through a terminal tool
|
|
5
5
|
* (`submit_plan`, `submit_profile`, `submit_synthesis`, …):
|
|
6
6
|
* the loop's text reply is discarded and the outcome is read from a closure
|
|
7
|
-
* cell the terminal tool writes.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* cell the terminal tool writes. The wrap-up of a capped run lets no tool run,
|
|
8
|
+
* so an agent that burns its whole iteration budget — or simply stops on
|
|
9
|
+
* prose — without ever submitting leaves that cell empty and forces the caller
|
|
10
|
+
* to hard-fail.
|
|
11
11
|
*
|
|
12
12
|
* `runToTerminal` runs the agent, then — if the outcome cell is still empty
|
|
13
|
-
* and the run was not aborted —
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* and the run was not aborted — runs ONE salvage continuation of the same
|
|
14
|
+
* conversation (`continueAgent`), opened by a corrective nudge. The salvage
|
|
15
|
+
* still declares every tool of the agent, masked to the terminal tools only,
|
|
16
|
+
* because the tool set is part of the prefix a signed thinking block and the
|
|
17
|
+
* prompt cache bind to.
|
|
18
18
|
*
|
|
19
19
|
* Salvage steps are namespaced (`salvage:…`) so a durable (DBOS) caller does
|
|
20
20
|
* not collide the continuation's `llm-*` / `tool-*` cache keys with the first
|
|
21
21
|
* run's — replaying them would silently return the first run's cached results.
|
|
22
22
|
*/
|
|
23
23
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
24
|
+
import { continueAgent } from "./continue-agent.js";
|
|
24
25
|
import { addChatUsage } from "./metrics.js";
|
|
25
|
-
import {
|
|
26
|
+
import { runAgent } from "./run-agent.js";
|
|
26
27
|
/** Default salvage budget: one submit plus a validation-fix retry or two. */
|
|
27
28
|
export const DEFAULT_SALVAGE_ITERATIONS = 3;
|
|
28
|
-
function salvageStepNames(base) {
|
|
29
|
-
return {
|
|
30
|
-
llm: (i) => `salvage:${base.llm(i)}`,
|
|
31
|
-
tool: (name, id) => `salvage:${base.tool(name, id)}`,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
29
|
/**
|
|
35
|
-
* Drive `agent` to its terminal tool, salvaging once if it
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
30
|
+
* Drive `agent` to its terminal tool, salvaging once if it does not get there.
|
|
31
|
+
* A salvage result carries both runs' messages and summed rollups, with the
|
|
32
|
+
* salvage's own finish. Throws first if a terminal tool is not declared on the
|
|
33
|
+
* agent.
|
|
39
34
|
*/
|
|
40
35
|
export async function runToTerminal(agent, initial, session, opts, salvage) {
|
|
36
|
+
const undeclared = salvage.tools.find((terminal) => !agent.tools.some((tool) => tool.id === terminal.id));
|
|
37
|
+
if (undeclared !== undefined) {
|
|
38
|
+
throw new Error(`The terminal tool "${undeclared.id}" is not a declared tool of the agent "${agent.id}", thus the salvage mask cannot let it run.`);
|
|
39
|
+
}
|
|
41
40
|
const first = await runAgent(agent, initial, session, opts);
|
|
42
41
|
if (opts.resolved?.() || opts.signal.aborted)
|
|
43
42
|
return { ...first, salvage: null };
|
|
44
43
|
const salvageBudget = salvage.maxIterations ?? DEFAULT_SALVAGE_ITERATIONS;
|
|
45
44
|
// Reported here rather than in `runAgent` because the loop cannot know it is being
|
|
46
|
-
// salvaged: it sees
|
|
45
|
+
// salvaged: it sees a continuation with a small cap and a mask.
|
|
47
46
|
// Only this wrapper holds the fact that a first attempt ended without its outcome.
|
|
48
47
|
// The first run's finish rides along because it is the whole diagnosis of WHY a
|
|
49
48
|
// salvage was needed, and it is the field the second run's result overwrites.
|
|
@@ -55,26 +54,16 @@ export async function runToTerminal(agent, initial, session, opts, salvage) {
|
|
|
55
54
|
salvageTools: salvage.tools.map((t) => t.id),
|
|
56
55
|
salvageMaxIterations: salvageBudget,
|
|
57
56
|
});
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
maxIterations: salvageBudget,
|
|
62
|
-
};
|
|
63
|
-
const salvageOpts = {
|
|
64
|
-
...opts,
|
|
65
|
-
formatStepName: salvageStepNames(opts.formatStepName ?? DEFAULT_STEP_NAME_FORMATTER),
|
|
66
|
-
};
|
|
67
|
-
const salvaged = await runAgent(salvageAgent, [...first.messages, { role: "user", content: salvage.nudge }], session, salvageOpts);
|
|
68
|
-
// The continuation is the same logical run as the first attempt — its
|
|
69
|
-
// message array already carries the first run's — so its rollups must too,
|
|
70
|
-
// or the caller reads the salvage turn's tokens as the whole cost.
|
|
57
|
+
const salvaged = await continueAgent(agent, first.messages, { text: salvage.nudge, mask: { allow: salvage.tools.map((t) => t.id) }, maxRequests: salvageBudget, stepNamespace: "salvage" }, session, opts);
|
|
58
|
+
// The continuation is the same logical run as the first attempt, so its
|
|
59
|
+
// rollups must cover both, or the caller reads the salvage's tokens as the whole cost.
|
|
71
60
|
//
|
|
72
61
|
// `salvage` deliberately keeps each attempt's OWN finish, unsummed: it is the
|
|
73
62
|
// per-attempt diagnosis, not a second accounting of the same tokens. The two
|
|
74
63
|
// views are therefore NOT additive — `finish` is the run's total and already
|
|
75
64
|
// covers everything `salvage.firstFinish` and `salvage.finish` report.
|
|
76
65
|
return {
|
|
77
|
-
...salvaged,
|
|
66
|
+
messages: [...first.messages, ...salvaged.messages],
|
|
78
67
|
finish: {
|
|
79
68
|
...salvaged.finish,
|
|
80
69
|
...sumUsage("usage", first.finish, salvaged.finish),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A signed thinking block is bound to the exact prefix of its request, so a
|
|
3
|
+
* request always declares the full tool set
|
|
4
|
+
* of its agent; the mask and the budget act at dispatch instead. Provider-native
|
|
5
|
+
* masking is out of scope, because the vendors do it differently and an
|
|
6
|
+
* OpenAI-compatible endpoint has neither form.
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolCallPart } from "ai";
|
|
9
|
+
import type { Tool } from "../tools/define-tool.js";
|
|
10
|
+
/** An absent mask lets each declared tool run. */
|
|
11
|
+
export type ToolMask = "none" | {
|
|
12
|
+
readonly allow: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
/** A tool with no entry has no limit. */
|
|
15
|
+
export type ToolBudget = Readonly<Record<string, number>>;
|
|
16
|
+
export declare function maskExcept(tools: readonly Tool[], ids: readonly string[]): ToolMask;
|
|
17
|
+
/**
|
|
18
|
+
* `undefined` for a call that passes, otherwise the model-visible refusal
|
|
19
|
+
* text. A passing call takes one unit from `used` regardless of its later
|
|
20
|
+
* result, because the check reads only the mask, the budget, and the run's
|
|
21
|
+
* calls — so a durable replay refuses the same calls again.
|
|
22
|
+
*/
|
|
23
|
+
export declare function refusalsFor(calls: readonly ToolCallPart[], mask: ToolMask | undefined, budget: ToolBudget | undefined, used: Map<string, number>): (string | undefined)[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A signed thinking block is bound to the exact prefix of its request, so a
|
|
3
|
+
* request always declares the full tool set
|
|
4
|
+
* of its agent; the mask and the budget act at dispatch instead. Provider-native
|
|
5
|
+
* masking is out of scope, because the vendors do it differently and an
|
|
6
|
+
* OpenAI-compatible endpoint has neither form.
|
|
7
|
+
*/
|
|
8
|
+
export function maskExcept(tools, ids) {
|
|
9
|
+
return { allow: tools.map((t) => t.id).filter((id) => !ids.includes(id)) };
|
|
10
|
+
}
|
|
11
|
+
function maskLets(mask, toolName) {
|
|
12
|
+
if (mask === undefined)
|
|
13
|
+
return true;
|
|
14
|
+
if (mask === "none")
|
|
15
|
+
return false;
|
|
16
|
+
return mask.allow.includes(toolName);
|
|
17
|
+
}
|
|
18
|
+
/** Matches the `{ error, retryable }` shape of every other tool error. */
|
|
19
|
+
function refusal(error) {
|
|
20
|
+
return JSON.stringify({ error, retryable: false });
|
|
21
|
+
}
|
|
22
|
+
function maskRefusal(mask, toolName) {
|
|
23
|
+
if (mask === "none")
|
|
24
|
+
return refusal(`No tool can run for this request, thus ${toolName} did not run. Answer in text.`);
|
|
25
|
+
const available = mask.allow.length === 0 ? "none" : mask.allow.join(", ");
|
|
26
|
+
return refusal(`The tool ${toolName} is not available for this request, thus it did not run. The tools that can run now: ${available}.`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `undefined` for a call that passes, otherwise the model-visible refusal
|
|
30
|
+
* text. A passing call takes one unit from `used` regardless of its later
|
|
31
|
+
* result, because the check reads only the mask, the budget, and the run's
|
|
32
|
+
* calls — so a durable replay refuses the same calls again.
|
|
33
|
+
*/
|
|
34
|
+
export function refusalsFor(calls, mask, budget, used) {
|
|
35
|
+
return calls.map((call) => {
|
|
36
|
+
if (mask !== undefined && !maskLets(mask, call.toolName))
|
|
37
|
+
return maskRefusal(mask, call.toolName);
|
|
38
|
+
const limit = budget?.[call.toolName];
|
|
39
|
+
const count = used.get(call.toolName) ?? 0;
|
|
40
|
+
if (limit !== undefined && count >= limit) {
|
|
41
|
+
return refusal(`The tool ${call.toolName} reached its limit of ${limit} calls in this run, thus this call did not run.`);
|
|
42
|
+
}
|
|
43
|
+
used.set(call.toolName, count + 1);
|
|
44
|
+
return undefined;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cut of a long tool result: the excerpt that the model reads, and the store that keeps the whole text.
|
|
3
|
+
* Each length and each offset counts UTF-16 code units, the unit of a JavaScript string length.
|
|
4
|
+
*/
|
|
5
|
+
import type { ToolResultPart } from "ai";
|
|
6
|
+
import type { ResultAsync } from "neverthrow";
|
|
7
|
+
import type { DomainError } from "../lib/result.js";
|
|
8
|
+
/** Keeps each skill file whole, because the largest one gives about 29,300 characters. */
|
|
9
|
+
export declare const TOOL_RESULT_CAP: number;
|
|
10
|
+
/** The start of an excerpt holds the shape of a result: its status, its first keys, or a header row. */
|
|
11
|
+
export declare const EXCERPT_HEAD_CHARS: number;
|
|
12
|
+
/** The end of an excerpt holds the errors and the stream flags of a command, thus it gets two times the start. */
|
|
13
|
+
export declare const EXCERPT_TAIL_CHARS: number;
|
|
14
|
+
/** The bound of a file that `grep` reads, thus a pattern search over a kept text costs no more than a `grep`. */
|
|
15
|
+
export declare const TOOL_OUTPUT_KEEP_MAX: number;
|
|
16
|
+
export declare const READ_TOOL_OUTPUT_TOOL_ID = "read_tool_output";
|
|
17
|
+
export interface KeptToolOutput {
|
|
18
|
+
readonly analysisId: string;
|
|
19
|
+
readonly ref: string;
|
|
20
|
+
readonly toolName: string;
|
|
21
|
+
readonly toolCallId: string;
|
|
22
|
+
/** Absent for a loop of a run, because a run belongs to its analysis and not to a thread. */
|
|
23
|
+
readonly threadId?: string;
|
|
24
|
+
/** The whole text, or its two halves with a marker line between them. */
|
|
25
|
+
readonly content: string;
|
|
26
|
+
/** The length of the whole text of the result. */
|
|
27
|
+
readonly totalLength: number;
|
|
28
|
+
}
|
|
29
|
+
/** Keeps the text of each cut tool result for `read_tool_output`. */
|
|
30
|
+
export interface ToolOutputStore {
|
|
31
|
+
/** A second put of the same analysis id and reference replaces the text. */
|
|
32
|
+
put(output: KeptToolOutput): ResultAsync<void, DomainError>;
|
|
33
|
+
get(analysisId: string, ref: string): ResultAsync<KeptToolOutput | null, DomainError>;
|
|
34
|
+
}
|
|
35
|
+
/** The reference of one kept text. It is short, because the model copies it. */
|
|
36
|
+
export declare function toolOutputRef(key: string): string;
|
|
37
|
+
/** The text that the model reads for a result, or `undefined` for a denial, which is never cut. */
|
|
38
|
+
export declare function resultTextOf(output: ToolResultPart["output"]): string | undefined;
|
|
39
|
+
/** Move `index` by one unit toward `direction` when it falls between the two halves of a surrogate pair. */
|
|
40
|
+
export declare function pairSafeIndex(text: string, index: number, direction: -1 | 1): number;
|
|
41
|
+
export declare function keptTextOf(text: string): string;
|
|
42
|
+
/** The excerpt of a text longer than `TOOL_RESULT_CAP`. `kept` is absent when the loop kept no text. */
|
|
43
|
+
export declare function excerptOf(text: string, kept: {
|
|
44
|
+
readonly ref: string;
|
|
45
|
+
readonly keptLength: number;
|
|
46
|
+
} | undefined): string;
|
|
47
|
+
/** The result with `excerpt` in place of its text. A part of a JSON text is not valid JSON, thus a JSON result becomes text. */
|
|
48
|
+
export declare function withExcerpt(part: ToolResultPart, excerpt: string): ToolResultPart;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cut of a long tool result: the excerpt that the model reads, and the store that keeps the whole text.
|
|
3
|
+
* Each length and each offset counts UTF-16 code units, the unit of a JavaScript string length.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
/** Keeps each skill file whole, because the largest one gives about 29,300 characters. */
|
|
7
|
+
export const TOOL_RESULT_CAP = 32 * 1024;
|
|
8
|
+
/** The start of an excerpt holds the shape of a result: its status, its first keys, or a header row. */
|
|
9
|
+
export const EXCERPT_HEAD_CHARS = 4 * 1024;
|
|
10
|
+
/** The end of an excerpt holds the errors and the stream flags of a command, thus it gets two times the start. */
|
|
11
|
+
export const EXCERPT_TAIL_CHARS = 8 * 1024;
|
|
12
|
+
/** The bound of a file that `grep` reads, thus a pattern search over a kept text costs no more than a `grep`. */
|
|
13
|
+
export const TOOL_OUTPUT_KEEP_MAX = 1024 * 1024;
|
|
14
|
+
export const READ_TOOL_OUTPUT_TOOL_ID = "read_tool_output";
|
|
15
|
+
/** The reference of one kept text. It is short, because the model copies it. */
|
|
16
|
+
export function toolOutputRef(key) {
|
|
17
|
+
return `to_${createHash("sha256").update(key).digest("hex").slice(0, 20)}`;
|
|
18
|
+
}
|
|
19
|
+
/** The text that the model reads for a result, or `undefined` for a denial, which is never cut. */
|
|
20
|
+
export function resultTextOf(output) {
|
|
21
|
+
switch (output.type) {
|
|
22
|
+
case "json":
|
|
23
|
+
case "error-json":
|
|
24
|
+
return JSON.stringify(output.value);
|
|
25
|
+
case "text":
|
|
26
|
+
case "error-text":
|
|
27
|
+
return output.value;
|
|
28
|
+
case "content":
|
|
29
|
+
return output.value.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
30
|
+
case "execution-denied":
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Move `index` by one unit toward `direction` when it falls between the two halves of a surrogate pair. */
|
|
35
|
+
export function pairSafeIndex(text, index, direction) {
|
|
36
|
+
if (index <= 0 || index >= text.length)
|
|
37
|
+
return index;
|
|
38
|
+
const before = text.charCodeAt(index - 1);
|
|
39
|
+
const after = text.charCodeAt(index);
|
|
40
|
+
const splitsPair = before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
|
|
41
|
+
return splitsPair ? index + direction : index;
|
|
42
|
+
}
|
|
43
|
+
export function keptTextOf(text) {
|
|
44
|
+
if (text.length <= TOOL_OUTPUT_KEEP_MAX)
|
|
45
|
+
return text;
|
|
46
|
+
const headEnd = pairSafeIndex(text, TOOL_OUTPUT_KEEP_MAX / 2, -1);
|
|
47
|
+
const tailStart = pairSafeIndex(text, text.length - TOOL_OUTPUT_KEEP_MAX / 2, 1);
|
|
48
|
+
return `${text.slice(0, headEnd)}\n[... ${tailStart - headEnd} characters not kept ...]\n${text.slice(tailStart)}`;
|
|
49
|
+
}
|
|
50
|
+
/** The excerpt of a text longer than `TOOL_RESULT_CAP`. `kept` is absent when the loop kept no text. */
|
|
51
|
+
export function excerptOf(text, kept) {
|
|
52
|
+
const headEnd = pairSafeIndex(text, EXCERPT_HEAD_CHARS, -1);
|
|
53
|
+
const tailStart = pairSafeIndex(text, text.length - EXCERPT_TAIL_CHARS, 1);
|
|
54
|
+
const shown = `Shown: the first ${headEnd} and the last ${text.length - tailStart} characters.`;
|
|
55
|
+
const lines = [
|
|
56
|
+
`[Tool result cut: ${text.length} characters, over the limit of ${TOOL_RESULT_CAP}. ${shown}${kept === undefined ? " The rest is not kept." : ""}]`,
|
|
57
|
+
];
|
|
58
|
+
if (kept !== undefined) {
|
|
59
|
+
lines.push(`[The harness kept ${kept.keptLength} characters as reference "${kept.ref}". Call ${READ_TOOL_OUTPUT_TOOL_ID} with this reference and an offset and a limit, or with a pattern. Offsets start at 0.]`);
|
|
60
|
+
}
|
|
61
|
+
lines.push(text.slice(0, headEnd), `[... ${tailStart - headEnd} characters not shown ...]`, text.slice(tailStart));
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
64
|
+
/** The result with `excerpt` in place of its text. A part of a JSON text is not valid JSON, thus a JSON result becomes text. */
|
|
65
|
+
export function withExcerpt(part, excerpt) {
|
|
66
|
+
const { output } = part;
|
|
67
|
+
switch (output.type) {
|
|
68
|
+
case "json":
|
|
69
|
+
case "text":
|
|
70
|
+
return { ...part, output: { type: "text", value: excerpt } };
|
|
71
|
+
case "error-json":
|
|
72
|
+
case "error-text":
|
|
73
|
+
return { ...part, output: { type: "error-text", value: excerpt } };
|
|
74
|
+
case "content":
|
|
75
|
+
return { ...part, output: { type: "content", value: [{ type: "text", text: excerpt }, ...output.value.filter((p) => p.type !== "text")] } };
|
|
76
|
+
case "execution-denied":
|
|
77
|
+
return part;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/loop/types.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface AgentDefinition {
|
|
|
26
26
|
readonly systemPrompt: string;
|
|
27
27
|
readonly model: string;
|
|
28
28
|
readonly tools: readonly Tool[];
|
|
29
|
-
/** Runaway guard
|
|
29
|
+
/** Runaway guard: at the cap the loop runs up to two masked wrap-up requests, until it gets a text answer. */
|
|
30
30
|
readonly maxIterations: number;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
@@ -52,9 +52,12 @@ export interface EventSource {
|
|
|
52
52
|
export type EmitEvent = {
|
|
53
53
|
readonly type: "iteration";
|
|
54
54
|
readonly source: EventSource;
|
|
55
|
-
/** Zero-based
|
|
55
|
+
/** Zero-based. Wrap-up request `k` has index `maxIterations + k`; a continuation counts from 0. */
|
|
56
56
|
readonly index: number;
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* True when this iteration produced the loop's terminal reply, including
|
|
59
|
+
* the last wrap-up even when its reply called a tool.
|
|
60
|
+
*/
|
|
58
61
|
readonly final: boolean;
|
|
59
62
|
} | {
|
|
60
63
|
readonly type: "tool-started";
|
|
@@ -35,6 +35,23 @@ export declare const SYNTHETIC_MESSAGE_KEY = "synthetic";
|
|
|
35
35
|
export declare const SYNTHETIC_RECORD_KEY = "syntheticRecord";
|
|
36
36
|
/** The {@link HARNESS_PROVIDER_NAMESPACE} key marking an assistant message whose production a client abort cut off. */
|
|
37
37
|
export declare const INTERRUPTED_MESSAGE_KEY = "interrupted";
|
|
38
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the kind of a context record. */
|
|
39
|
+
export declare const CONTEXT_KIND_KEY = "contextKind";
|
|
40
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the SHA-256 hex hash of the text of a context record. */
|
|
41
|
+
export declare const CONTEXT_HASH_KEY = "contextHash";
|
|
42
|
+
declare const CONTEXT_KINDS: readonly ["analysis-context", "run-activity", "working-memory"];
|
|
43
|
+
/** The kinds of per-turn context that a chat turn stores after its user message. */
|
|
44
|
+
export type ContextKind = (typeof CONTEXT_KINDS)[number];
|
|
45
|
+
/**
|
|
46
|
+
* A context record: a synthetic `user` message, and no host record, thus it opens no turn and the
|
|
47
|
+
* display does not show it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function contextRecordMessage(kind: ContextKind, text: string): ModelMessage;
|
|
50
|
+
/** The kind and the hash of a context record, or `undefined` for a message that is no record. */
|
|
51
|
+
export declare function contextRecordOf(message: ModelMessage): {
|
|
52
|
+
readonly kind: ContextKind;
|
|
53
|
+
readonly hash: string;
|
|
54
|
+
} | undefined;
|
|
38
55
|
/**
|
|
39
56
|
* Return a copy of `message` carrying the interruption marker in the harness namespace.
|
|
40
57
|
*
|
|
@@ -57,7 +74,7 @@ export declare function isInterruptedMessage(message: ModelMessage): boolean;
|
|
|
57
74
|
* It has to carry the `user` role because the wire format requires a user turn after a truncated
|
|
58
75
|
* assistant message, but it is not user input, and the difference is load-bearing: a `user` message is
|
|
59
76
|
* what OPENS a conversation turn, so an unmarked synthetic one reads as a spurious turn boundary —
|
|
60
|
-
* splitting one turn into two for the
|
|
77
|
+
* splitting one turn into two for the view and, worse, giving a tail-turn removal a cut point in
|
|
61
78
|
* the middle of a turn. Marking it is what lets {@link isSyntheticUserMessage} keep those readers honest.
|
|
62
79
|
*
|
|
63
80
|
* `content` takes prose or the parts of a user message, because a picture rides a file part and no
|
|
@@ -74,7 +91,7 @@ export declare function syntheticUserMessage(content: UserContent): ModelMessage
|
|
|
74
91
|
* outcome, which the model should read on its next turn but which nobody said.
|
|
75
92
|
*
|
|
76
93
|
* Synthetic in every sense that matters to a turn-boundary reader ({@link isSyntheticUserMessage} is
|
|
77
|
-
* true of it), so it cannot split a turn for the
|
|
94
|
+
* true of it), so it cannot split a turn for the view nor hand a tail retraction a mid-turn
|
|
78
95
|
* cut point. It differs from {@link syntheticUserMessage} in exactly one respect, and it is the
|
|
79
96
|
* decisive one: this is a fact the reader is entitled to see, so the display reconstruction emits it
|
|
80
97
|
* — as a `system` message, which is what it is — rather than dropping it as loop machinery.
|
|
@@ -89,6 +106,36 @@ export declare function syntheticRecordMessage(text: string): ModelMessage;
|
|
|
89
106
|
export declare function isSyntheticUserMessage(message: ModelMessage): boolean;
|
|
90
107
|
/** Whether this is a host-appended record of out-of-band work (see {@link syntheticRecordMessage}) — synthetic, but displayed. */
|
|
91
108
|
export declare function isSyntheticRecordMessage(message: ModelMessage): boolean;
|
|
109
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the compaction id on each message of its exchange, which the view rule skips. */
|
|
110
|
+
export declare const COMPACTION_EXCHANGE_KEY = "compactionExchange";
|
|
111
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the figures of a compaction marker, which the view rule reads. */
|
|
112
|
+
export declare const COMPACTION_MARKER_KEY = "compactionMarker";
|
|
113
|
+
declare const CompactionMarkerSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
114
|
+
id: z.ZodString;
|
|
115
|
+
tokensBefore: z.ZodNumber;
|
|
116
|
+
tokensAfter: z.ZodNumber;
|
|
117
|
+
durationMs: z.ZodNumber;
|
|
118
|
+
kind: z.ZodLiteral<"summary">;
|
|
119
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
120
|
+
keptTurns: z.ZodNumber;
|
|
121
|
+
id: z.ZodString;
|
|
122
|
+
tokensBefore: z.ZodNumber;
|
|
123
|
+
tokensAfter: z.ZodNumber;
|
|
124
|
+
durationMs: z.ZodNumber;
|
|
125
|
+
kind: z.ZodLiteral<"drop">;
|
|
126
|
+
}, z.core.$strip>], "kind">;
|
|
127
|
+
export type CompactionMarker = z.infer<typeof CompactionMarkerSchema>;
|
|
128
|
+
/** A copy of `message` whose harness namespace, which no provider reads, holds the compaction id. */
|
|
129
|
+
export declare function markCompactionExchange(message: ModelMessage, id: string): ModelMessage;
|
|
130
|
+
export declare function compactionExchangeOf(message: ModelMessage): string | undefined;
|
|
131
|
+
export declare function summaryMarkerMessage(summary: string, marker: Extract<CompactionMarker, {
|
|
132
|
+
kind: "summary";
|
|
133
|
+
}>): ModelMessage;
|
|
134
|
+
/** The marker of a compaction that gave no summary. No view holds it. */
|
|
135
|
+
export declare function dropMarkerMessage(marker: Extract<CompactionMarker, {
|
|
136
|
+
kind: "drop";
|
|
137
|
+
}>): ModelMessage;
|
|
138
|
+
export declare function compactionMarkerOf(message: ModelMessage): CompactionMarker | undefined;
|
|
92
139
|
type LegacyContent = string | Array<Record<string, unknown>>;
|
|
93
140
|
export interface LegacyMessageRow {
|
|
94
141
|
readonly threadId: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { modelMessageSchema } from "ai";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
export const SUPPORTED_AI_SDK_MAJOR = 7;
|
|
@@ -46,6 +47,35 @@ export const SYNTHETIC_MESSAGE_KEY = "synthetic";
|
|
|
46
47
|
export const SYNTHETIC_RECORD_KEY = "syntheticRecord";
|
|
47
48
|
/** The {@link HARNESS_PROVIDER_NAMESPACE} key marking an assistant message whose production a client abort cut off. */
|
|
48
49
|
export const INTERRUPTED_MESSAGE_KEY = "interrupted";
|
|
50
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the kind of a context record. */
|
|
51
|
+
export const CONTEXT_KIND_KEY = "contextKind";
|
|
52
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the SHA-256 hex hash of the text of a context record. */
|
|
53
|
+
export const CONTEXT_HASH_KEY = "contextHash";
|
|
54
|
+
const CONTEXT_KINDS = ["analysis-context", "run-activity", "working-memory"];
|
|
55
|
+
/**
|
|
56
|
+
* A context record: a synthetic `user` message, and no host record, thus it opens no turn and the
|
|
57
|
+
* display does not show it.
|
|
58
|
+
*/
|
|
59
|
+
export function contextRecordMessage(kind, text) {
|
|
60
|
+
const hash = createHash("sha256").update(text).digest("hex");
|
|
61
|
+
return {
|
|
62
|
+
role: "user",
|
|
63
|
+
content: text,
|
|
64
|
+
providerOptions: { [HARNESS_PROVIDER_NAMESPACE]: { [SYNTHETIC_MESSAGE_KEY]: true, [CONTEXT_KIND_KEY]: kind, [CONTEXT_HASH_KEY]: hash } },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function isContextKind(value) {
|
|
68
|
+
return CONTEXT_KINDS.some((kind) => kind === value);
|
|
69
|
+
}
|
|
70
|
+
/** The kind and the hash of a context record, or `undefined` for a message that is no record. */
|
|
71
|
+
export function contextRecordOf(message) {
|
|
72
|
+
const harness = message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE];
|
|
73
|
+
const kind = harness?.[CONTEXT_KIND_KEY];
|
|
74
|
+
const hash = harness?.[CONTEXT_HASH_KEY];
|
|
75
|
+
if (typeof hash !== "string" || !isContextKind(kind))
|
|
76
|
+
return undefined;
|
|
77
|
+
return { kind, hash };
|
|
78
|
+
}
|
|
49
79
|
/**
|
|
50
80
|
* Return a copy of `message` carrying the interruption marker in the harness namespace.
|
|
51
81
|
*
|
|
@@ -80,7 +110,7 @@ export function isInterruptedMessage(message) {
|
|
|
80
110
|
* It has to carry the `user` role because the wire format requires a user turn after a truncated
|
|
81
111
|
* assistant message, but it is not user input, and the difference is load-bearing: a `user` message is
|
|
82
112
|
* what OPENS a conversation turn, so an unmarked synthetic one reads as a spurious turn boundary —
|
|
83
|
-
* splitting one turn into two for the
|
|
113
|
+
* splitting one turn into two for the view and, worse, giving a tail-turn removal a cut point in
|
|
84
114
|
* the middle of a turn. Marking it is what lets {@link isSyntheticUserMessage} keep those readers honest.
|
|
85
115
|
*
|
|
86
116
|
* `content` takes prose or the parts of a user message, because a picture rides a file part and no
|
|
@@ -103,7 +133,7 @@ export function syntheticUserMessage(content) {
|
|
|
103
133
|
* outcome, which the model should read on its next turn but which nobody said.
|
|
104
134
|
*
|
|
105
135
|
* Synthetic in every sense that matters to a turn-boundary reader ({@link isSyntheticUserMessage} is
|
|
106
|
-
* true of it), so it cannot split a turn for the
|
|
136
|
+
* true of it), so it cannot split a turn for the view nor hand a tail retraction a mid-turn
|
|
107
137
|
* cut point. It differs from {@link syntheticUserMessage} in exactly one respect, and it is the
|
|
108
138
|
* decisive one: this is a fact the reader is entitled to see, so the display reconstruction emits it
|
|
109
139
|
* — as a `system` message, which is what it is — rather than dropping it as loop machinery.
|
|
@@ -128,6 +158,52 @@ export function isSyntheticUserMessage(message) {
|
|
|
128
158
|
export function isSyntheticRecordMessage(message) {
|
|
129
159
|
return message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE]?.[SYNTHETIC_RECORD_KEY] === true;
|
|
130
160
|
}
|
|
161
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the compaction id on each message of its exchange, which the view rule skips. */
|
|
162
|
+
export const COMPACTION_EXCHANGE_KEY = "compactionExchange";
|
|
163
|
+
/** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the figures of a compaction marker, which the view rule reads. */
|
|
164
|
+
export const COMPACTION_MARKER_KEY = "compactionMarker";
|
|
165
|
+
const compactionFigures = { id: z.string(), tokensBefore: z.number(), tokensAfter: z.number(), durationMs: z.number() };
|
|
166
|
+
const CompactionMarkerSchema = z.discriminatedUnion("kind", [
|
|
167
|
+
z.object({ kind: z.literal("summary"), ...compactionFigures }),
|
|
168
|
+
z.object({ kind: z.literal("drop"), ...compactionFigures, keptTurns: z.number() }),
|
|
169
|
+
]);
|
|
170
|
+
/** A copy of `message` whose harness namespace, which no provider reads, holds the compaction id. */
|
|
171
|
+
export function markCompactionExchange(message, id) {
|
|
172
|
+
const existingOptions = message.providerOptions ?? {};
|
|
173
|
+
return {
|
|
174
|
+
...message,
|
|
175
|
+
providerOptions: {
|
|
176
|
+
...existingOptions,
|
|
177
|
+
[HARNESS_PROVIDER_NAMESPACE]: { ...existingOptions[HARNESS_PROVIDER_NAMESPACE], [COMPACTION_EXCHANGE_KEY]: id },
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function compactionExchangeOf(message) {
|
|
182
|
+
const id = message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE]?.[COMPACTION_EXCHANGE_KEY];
|
|
183
|
+
return typeof id === "string" ? id : undefined;
|
|
184
|
+
}
|
|
185
|
+
// A marker is synthetic, thus it opens no turn. It is no host record, because its divider shows it.
|
|
186
|
+
function compactionMarkerMessage(text, marker) {
|
|
187
|
+
return {
|
|
188
|
+
role: "user",
|
|
189
|
+
content: text,
|
|
190
|
+
providerOptions: { [HARNESS_PROVIDER_NAMESPACE]: { [SYNTHETIC_MESSAGE_KEY]: true, [COMPACTION_MARKER_KEY]: marker } },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export function summaryMarkerMessage(summary, marker) {
|
|
194
|
+
return compactionMarkerMessage(`[Conversation Summary]\n${summary}`, marker);
|
|
195
|
+
}
|
|
196
|
+
/** The marker of a compaction that gave no summary. No view holds it. */
|
|
197
|
+
export function dropMarkerMessage(marker) {
|
|
198
|
+
return compactionMarkerMessage("[Compaction Failed]", marker);
|
|
199
|
+
}
|
|
200
|
+
export function compactionMarkerOf(message) {
|
|
201
|
+
const value = message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE]?.[COMPACTION_MARKER_KEY];
|
|
202
|
+
if (value === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
const parsed = CompactionMarkerSchema.safeParse(value);
|
|
205
|
+
return parsed.success ? parsed.data : undefined;
|
|
206
|
+
}
|
|
131
207
|
function providerOptionsFromLegacy(block) {
|
|
132
208
|
// A block may carry both a signed thinking signature and cache_control;
|
|
133
209
|
// both are required for valid continuation, so both must survive.
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import type { ModelMessage } from "ai";
|
|
1
2
|
import type { EmitFn } from "../loop/types.js";
|
|
2
3
|
import { type ConversationUIMessage } from "./conversation-display-storage.js";
|
|
3
4
|
export interface ConversationDisplayRecorder {
|
|
4
5
|
readonly emit: EmitFn;
|
|
6
|
+
/** The user message of the turn. */
|
|
7
|
+
takeOpening(): ConversationUIMessage[];
|
|
8
|
+
/** The assistant message of one round, when the recorder got parts after the last take, and the divider of a marker of the round. */
|
|
9
|
+
takeRound(messages: readonly ModelMessage[]): ConversationUIMessage[];
|
|
10
|
+
/** The whole turn in one projection. `runChatTurn` stores each round with `takeOpening` and `takeRound` instead. */
|
|
5
11
|
finish(options?: {
|
|
6
12
|
readonly fallbackText?: string;
|
|
7
13
|
readonly interrupted?: boolean;
|