@inflexa-ai/harness 0.44.0 → 0.45.1
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/dist/agents/conversation-agent.d.ts +10 -0
- package/dist/agents/conversation-agent.js +19 -1
- package/dist/agents/sandbox/shared.d.ts +23 -2
- package/dist/agents/sandbox/shared.js +51 -18
- package/dist/agents/sandbox/translational-safety-agent.js +26 -0
- package/dist/agents/sandbox/types.d.ts +24 -1
- package/dist/agents/sandbox-catalog.d.ts +14 -0
- package/dist/agents/sandbox-catalog.js +19 -0
- package/dist/app/approve-plan.d.ts +51 -0
- package/dist/app/approve-plan.js +84 -0
- package/dist/app/chat-turn.js +6 -3
- package/dist/app/data-profile-view.d.ts +1 -1
- package/dist/app/run-activity.js +4 -6
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +14 -2
- package/dist/contracts/chat-events.js +1 -1
- package/dist/contracts/chat-frame.d.ts +7 -0
- package/dist/contracts/chat-frame.js +24 -6
- package/dist/contracts/chat-parts.d.ts +73 -2
- package/dist/contracts/chat-parts.js +3 -1
- package/dist/contracts/index.d.ts +4 -2
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/part-registry.js +3 -0
- package/dist/contracts/schemas/chat-events.d.ts +38 -0
- package/dist/contracts/schemas/chat-events.js +8 -0
- package/dist/contracts/schemas/chat-parts.d.ts +239 -15
- package/dist/contracts/schemas/chat-parts.js +80 -12
- package/dist/contracts/schemas/index.d.ts +2 -2
- package/dist/contracts/schemas/index.js +2 -2
- package/dist/contracts/subagent-activity.d.ts +12 -0
- package/dist/contracts/subagent-activity.js +76 -0
- package/dist/execution/artifact-metadata.js +1 -3
- package/dist/execution/dbos-workflow-purger.d.ts +2 -0
- package/dist/execution/dbos-workflow-purger.js +21 -14
- package/dist/execution/run-canceler.d.ts +33 -11
- package/dist/execution/run-canceler.js +130 -28
- package/dist/execution/run-end-record.d.ts +30 -0
- package/dist/execution/run-end-record.js +27 -0
- package/dist/execution/run-event-parts.d.ts +6 -5
- package/dist/execution/run-event-parts.js +6 -5
- package/dist/execution/run-event-stream.d.ts +18 -6
- package/dist/execution/run-event-stream.js +67 -17
- package/dist/execution/run-synthesis.d.ts +21 -0
- package/dist/execution/run-synthesis.js +77 -3
- package/dist/index.d.ts +28 -10
- package/dist/index.js +26 -2
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/lib/metrics.d.ts +2 -2
- package/dist/loop/call-guard.d.ts +50 -0
- package/dist/loop/call-guard.js +90 -0
- package/dist/loop/compaction.d.ts +5 -0
- package/dist/loop/compaction.js +17 -0
- package/dist/loop/run-agent.d.ts +9 -0
- package/dist/loop/run-agent.js +16 -1
- package/dist/loop/run-to-terminal.js +4 -1
- package/dist/loop/types.d.ts +8 -0
- package/dist/memory/card-builders.d.ts +11 -0
- package/dist/memory/card-builders.js +35 -21
- package/dist/memory/conversation-display-recorder.d.ts +2 -2
- package/dist/memory/conversation-display-recorder.js +38 -4
- package/dist/memory/conversation-display-storage.d.ts +9 -2
- package/dist/memory/conversation-display-storage.js +18 -3
- package/dist/memory/thread-history.d.ts +42 -8
- package/dist/memory/thread-history.js +164 -45
- package/dist/memory/thread-store.d.ts +13 -0
- package/dist/memory/thread-store.js +15 -1
- package/dist/prompts/briefing.d.ts +57 -3
- package/dist/prompts/briefing.js +129 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +3 -1
- package/dist/prompts/planner.js +13 -2
- package/dist/provenance/seam.d.ts +39 -20
- package/dist/provenance/seam.js +7 -5
- package/dist/providers/ai-sdk.js +93 -11
- package/dist/providers/types.d.ts +6 -0
- package/dist/reference-data/catalog.js +141 -2
- package/dist/report-model/authoring-grammar.d.ts +26 -26
- package/dist/report-model/pin-snapshot.d.ts +1 -1
- package/dist/report-model/pin-snapshot.js +2 -50
- package/dist/runtime/assemble.d.ts +8 -1
- package/dist/runtime/assemble.js +11 -0
- package/dist/runtime/boot.d.ts +3 -0
- package/dist/runtime/boot.js +10 -1
- package/dist/sandbox/image-packages.d.ts +26 -4
- package/dist/sandbox/image-packages.js +18 -4
- package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
- package/dist/sandbox/sandbox-step-translate.js +24 -0
- package/dist/schemas/artifact-manifest.d.ts +1 -1
- package/dist/schemas/artifact-manifest.js +11 -9
- package/dist/schemas/data-profile-schemas.d.ts +12 -12
- package/dist/schemas/plan-schemas.d.ts +75 -21
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/schemas/workflow-state.d.ts +113 -0
- package/dist/schemas/workflow-state.js +54 -0
- package/dist/state/analyses-metrics.d.ts +79 -7
- package/dist/state/analyses-metrics.js +141 -6
- package/dist/state/artifacts.d.ts +66 -3
- package/dist/state/artifacts.js +85 -7
- package/dist/state/data-profile.d.ts +4 -2
- package/dist/state/data-profile.js +7 -5
- package/dist/state/index.d.ts +9 -8
- package/dist/state/index.js +5 -5
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
- package/dist/state/migrations/index.js +8 -0
- package/dist/state/plans.d.ts +21 -0
- package/dist/state/plans.js +25 -0
- package/dist/state/runs.d.ts +79 -14
- package/dist/state/runs.js +150 -33
- package/dist/state/schema.d.ts +2 -0
- package/dist/state/step-executions.d.ts +9 -0
- package/dist/state/step-executions.js +22 -0
- package/dist/tasks/data-profile-activity.d.ts +7 -1
- package/dist/tasks/data-profile-activity.js +18 -9
- package/dist/tasks/data-profile.d.ts +15 -1
- package/dist/tasks/data-profile.js +16 -11
- package/dist/tools/ad-hoc-router.d.ts +2 -0
- package/dist/tools/ad-hoc-router.js +18 -2
- package/dist/tools/approval/gateway.js +28 -5
- package/dist/tools/capability-data.d.ts +36 -0
- package/dist/tools/capability-data.js +73 -0
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +80 -1
- package/dist/tools/execute-analysis.js +352 -110
- package/dist/tools/knowledge/check.d.ts +60 -0
- package/dist/tools/knowledge/check.js +92 -0
- package/dist/tools/knowledge/client.d.ts +382 -0
- package/dist/tools/knowledge/client.js +302 -0
- package/dist/tools/knowledge/environment.d.ts +78 -0
- package/dist/tools/knowledge/environment.js +105 -0
- package/dist/tools/knowledge/index.d.ts +49 -0
- package/dist/tools/knowledge/index.js +70 -0
- package/dist/tools/knowledge/local-slots.d.ts +62 -0
- package/dist/tools/knowledge/local-slots.js +177 -0
- package/dist/tools/knowledge/pin.d.ts +14 -0
- package/dist/tools/knowledge/pin.js +17 -0
- package/dist/tools/knowledge/recommend.d.ts +92 -0
- package/dist/tools/knowledge/recommend.js +98 -0
- package/dist/tools/knowledge/situation.d.ts +143 -0
- package/dist/tools/knowledge/situation.js +164 -0
- package/dist/tools/knowledge/skeleton.d.ts +69 -0
- package/dist/tools/knowledge/skeleton.js +202 -0
- package/dist/tools/knowledge/template.d.ts +137 -0
- package/dist/tools/knowledge/template.js +267 -0
- package/dist/tools/research/generate-plan.d.ts +23 -0
- package/dist/tools/research/generate-plan.js +203 -12
- package/dist/tools/research/guarded-address.d.ts +20 -0
- package/dist/tools/research/guarded-address.js +148 -0
- package/dist/tools/research/inspect-data-profile.d.ts +1 -1
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/inspect-run.js +7 -7
- package/dist/tools/research/searxng-engine.d.ts +40 -0
- package/dist/tools/research/searxng-engine.js +120 -0
- package/dist/tools/research/web-fetch.d.ts +124 -0
- package/dist/tools/research/web-fetch.js +627 -0
- package/dist/tools/research/web-search.d.ts +75 -0
- package/dist/tools/research/web-search.js +112 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
- package/dist/tools/sandbox/list-available-packages.js +12 -4
- package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
- package/dist/tools/sandbox/list-available-refs.js +60 -3
- package/dist/tools/sandbox/report-blocker.js +5 -1
- package/dist/tools/terminal-text.d.ts +11 -0
- package/dist/tools/terminal-text.js +19 -0
- package/dist/tools/workspace/decision-record.d.ts +81 -0
- package/dist/tools/workspace/decision-record.js +117 -0
- package/dist/tools/workspace/edit-file.d.ts +3 -1
- package/dist/tools/workspace/edit-file.js +20 -2
- package/dist/tools/workspace/execute-command.d.ts +17 -0
- package/dist/tools/workspace/execute-command.js +28 -1
- package/dist/tools/workspace/index.d.ts +1 -0
- package/dist/tools/workspace/index.js +1 -0
- package/dist/tools/workspace/mutator.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +62 -21
- package/dist/workflows/execute-analysis.js +307 -79
- package/dist/workflows/plan/graph.d.ts +16 -0
- package/dist/workflows/plan/graph.js +36 -0
- package/dist/workflows/sandbox-step.d.ts +12 -0
- package/dist/workflows/sandbox-step.js +14 -12
- package/package.json +5 -1
|
@@ -10,6 +10,11 @@ export interface CompactionRules {
|
|
|
10
10
|
/** The budget before the user message of a turn joins the conversation. Absent: no compaction before the user message. */
|
|
11
11
|
readonly turnStartBudget?: number;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The rules of an agent for a model with `contextWindow`. The budget is the smaller of the declared budget and 80% of
|
|
15
|
+
* the window. The turn-start budget keeps its ratio to the budget. An absent window keeps the declared rules.
|
|
16
|
+
*/
|
|
17
|
+
export declare function rulesForContextWindow(rules: CompactionRules, contextWindow: number | undefined): CompactionRules;
|
|
13
18
|
/** How the loop compacts its conversation when the last request passed a budget. */
|
|
14
19
|
export interface CompactionPolicy extends CompactionRules {
|
|
15
20
|
/** The provider of the conversation, with no text stream to the surface: the summary is no reply. */
|
package/dist/loop/compaction.js
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
/** The cap of requests of one compaction exchange: up to 3 rounds of memory edits, and then the summary. */
|
|
2
2
|
export const COMPACTION_MAX_REQUESTS = 4;
|
|
3
|
+
/** The share of a context window that a request can fill before the loop compacts. */
|
|
4
|
+
const CONTEXT_WINDOW_SHARE = 0.8;
|
|
5
|
+
/**
|
|
6
|
+
* The rules of an agent for a model with `contextWindow`. The budget is the smaller of the declared budget and 80% of
|
|
7
|
+
* the window. The turn-start budget keeps its ratio to the budget. An absent window keeps the declared rules.
|
|
8
|
+
*/
|
|
9
|
+
export function rulesForContextWindow(rules, contextWindow) {
|
|
10
|
+
if (contextWindow === undefined)
|
|
11
|
+
return rules;
|
|
12
|
+
const budget = Math.min(rules.budget, Math.floor(contextWindow * CONTEXT_WINDOW_SHARE));
|
|
13
|
+
if (budget === rules.budget)
|
|
14
|
+
return rules;
|
|
15
|
+
return {
|
|
16
|
+
budget,
|
|
17
|
+
...(rules.turnStartBudget === undefined ? {} : { turnStartBudget: Math.floor((rules.turnStartBudget * budget) / rules.budget) }),
|
|
18
|
+
};
|
|
19
|
+
}
|
package/dist/loop/run-agent.d.ts
CHANGED
|
@@ -91,6 +91,15 @@ export interface RunAgentOptions {
|
|
|
91
91
|
* prompt cache and a signed thinking block bind to.
|
|
92
92
|
*/
|
|
93
93
|
readonly toolChoice?: ChatRequest["toolChoice"];
|
|
94
|
+
/**
|
|
95
|
+
* An early cap. Read at the top of each iteration; when it answers true the
|
|
96
|
+
* loop leaves the tool phase as if `maxIterations` were reached and takes the
|
|
97
|
+
* same tool-less wrap-up path, thus a terminal wrapper salvages the run the
|
|
98
|
+
* same way. A host uses it to end a run that a policy has judged to be a
|
|
99
|
+
* loop, for example one the call guard refused too many times, without
|
|
100
|
+
* waiting for the iteration cap or the wall clock.
|
|
101
|
+
*/
|
|
102
|
+
readonly stopWhen?: () => boolean;
|
|
94
103
|
/**
|
|
95
104
|
* Optional outcome predicate for loop-driving agents whose result is
|
|
96
105
|
* recorded by a tool into closure state. Checked immediately after each
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -97,6 +97,7 @@ export function openLoop(agent, initial, session, opts, metricAgentId) {
|
|
|
97
97
|
const source = {
|
|
98
98
|
agentId: session.provenance.agentId ?? agent.id,
|
|
99
99
|
callPath: session.provenance.callPath,
|
|
100
|
+
...(opts.invocationId === undefined ? {} : { invocationId: opts.invocationId }),
|
|
100
101
|
};
|
|
101
102
|
const policy = opts.compaction;
|
|
102
103
|
const viewOptions = { keepFirstTurn: policy?.keepFirstTurn === true };
|
|
@@ -152,6 +153,7 @@ export function openLoop(agent, initial, session, opts, metricAgentId) {
|
|
|
152
153
|
});
|
|
153
154
|
let iterations = 0;
|
|
154
155
|
let truncationRecoveries = 0;
|
|
156
|
+
let stoppedEarly = false;
|
|
155
157
|
// Counted rather than listed: the finish record stays one bounded line for a run of
|
|
156
158
|
// any length, and "50 iterations, 49 tool calls, 47 of them errored" already separates
|
|
157
159
|
// a productive long run from a loop stuck retrying one failing call. The per-iteration
|
|
@@ -371,9 +373,11 @@ export function openLoop(agent, initial, session, opts, metricAgentId) {
|
|
|
371
373
|
const records = await active.recordsAfter(conversationView([...messages, markerOf(0)], viewOptions).messages);
|
|
372
374
|
const durationMs = Math.round(performance.now() - startedAt);
|
|
373
375
|
messages.push(markerOf(durationMs), ...records);
|
|
374
|
-
await giveRound();
|
|
375
376
|
settled = true;
|
|
377
|
+
// The `done` part goes out before the marker round: a chat turn gives the rounds after a marker a new
|
|
378
|
+
// assistant message, and the `running` and `done` parts of one compaction must stream into the same message.
|
|
376
379
|
await emitPart({ id, status: "done", tokensBefore, durationMs });
|
|
380
|
+
await giveRound();
|
|
377
381
|
log.info("conversation compacted", { compactionId: id, trigger, tokensBefore, durationMs });
|
|
378
382
|
summarizedLast = true;
|
|
379
383
|
return undefined;
|
|
@@ -419,6 +423,13 @@ export function openLoop(agent, initial, session, opts, metricAgentId) {
|
|
|
419
423
|
if (segment.requestText !== undefined)
|
|
420
424
|
messages.push(syntheticUserMessage(segment.requestText));
|
|
421
425
|
for (let k = 0; k < segment.maxRequests; k++) {
|
|
426
|
+
// The early cap ends a segment before the cap as if the cap were reached, thus a run takes its
|
|
427
|
+
// tool-less wrap-up. The segment that closes a capped run is that wrap-up, and it runs to its end.
|
|
428
|
+
if (segment.closesCappedRun !== true && opts.stopWhen?.()) {
|
|
429
|
+
stoppedEarly = true;
|
|
430
|
+
log.warn("run stopped early by the host policy, taking the wrap-up path", { iterations });
|
|
431
|
+
return "capped";
|
|
432
|
+
}
|
|
422
433
|
const index = segment.firstIndex + k;
|
|
423
434
|
// The last request of a segment that closes a capped run ends the run,
|
|
424
435
|
// whatever its reply, thus its `iteration` event is the final one.
|
|
@@ -511,6 +522,8 @@ export function openLoop(agent, initial, session, opts, metricAgentId) {
|
|
|
511
522
|
// abort (user cut the turn) from a plain `max_iterations` cap-out.
|
|
512
523
|
const reason = reply.finishReason === "aborted" ? "aborted" : "max_iterations";
|
|
513
524
|
recordAgentRun({ agentId: metricAgentId, iterations, cappedOut: true });
|
|
525
|
+
if (stoppedEarly)
|
|
526
|
+
log.warn("run finished on the early cap", { iterations });
|
|
514
527
|
logFinish("warn", reason, true);
|
|
515
528
|
await giveRound();
|
|
516
529
|
return { messages, finish: { reason, cappedOut: true, truncationRecoveries, ...finishUsage() } };
|
|
@@ -551,6 +564,8 @@ export function openLoop(agent, initial, session, opts, metricAgentId) {
|
|
|
551
564
|
const endCapped = async () => {
|
|
552
565
|
settleTranscript();
|
|
553
566
|
recordAgentRun({ agentId: metricAgentId, iterations, cappedOut: true });
|
|
567
|
+
if (stoppedEarly)
|
|
568
|
+
log.warn("run finished on the early cap", { iterations });
|
|
554
569
|
logFinish("warn", "max_iterations", true);
|
|
555
570
|
await giveRound();
|
|
556
571
|
return { messages, finish: { reason: "max_iterations", cappedOut: true, truncationRecoveries, ...finishUsage() } };
|
|
@@ -54,7 +54,10 @@ export async function runToTerminal(agent, initial, session, opts, salvage) {
|
|
|
54
54
|
salvageTools: salvage.tools.map((t) => t.id),
|
|
55
55
|
salvageMaxIterations: salvageBudget,
|
|
56
56
|
});
|
|
57
|
-
|
|
57
|
+
// The early cap of the first run must not end the salvage turn, whose whole
|
|
58
|
+
// purpose is to submit after the first run stopped.
|
|
59
|
+
const { stopWhen: _stopWhen, ...continuation } = opts;
|
|
60
|
+
const salvaged = await continueAgent(agent, first.messages, { text: salvage.nudge, mask: { allow: salvage.tools.map((t) => t.id) }, maxRequests: salvageBudget, stepNamespace: "salvage" }, session, continuation);
|
|
58
61
|
// The continuation is the same logical run as the first attempt, so its
|
|
59
62
|
// rollups must cover both, or the caller reads the salvage's tokens as the whole cost.
|
|
60
63
|
//
|
package/dist/loop/types.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export type RunStep = <T>(name: string, fn: () => Promise<T>) => Promise<T>;
|
|
|
46
46
|
export interface EventSource {
|
|
47
47
|
readonly agentId: string;
|
|
48
48
|
readonly callPath: readonly string[];
|
|
49
|
+
/** The id of the tool call that runs this loop. For a data profile, the workflow id of its attempt. */
|
|
50
|
+
readonly invocationId?: string;
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Orchestration events the loop emits: iteration boundaries and tool-call
|
|
@@ -100,6 +102,12 @@ export type EmitEvent = {
|
|
|
100
102
|
* taken. As a result a host can fall back to its own observation.
|
|
101
103
|
*/
|
|
102
104
|
readonly durationMs?: number;
|
|
105
|
+
} | {
|
|
106
|
+
/** The display recorder of a chat turn emits it, not the loop. See `MessageStartEvent`. */
|
|
107
|
+
readonly type: "message-start";
|
|
108
|
+
readonly source: EventSource;
|
|
109
|
+
readonly messageId: string;
|
|
110
|
+
readonly role: "user" | "assistant";
|
|
103
111
|
};
|
|
104
112
|
/**
|
|
105
113
|
* A UI presentation event a tool streams to chat — an AI-SDK `data-*`
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
import { type ResultAsync } from "neverthrow";
|
|
17
17
|
import type { Pool } from "pg";
|
|
18
18
|
import { type DbError } from "../lib/db-result.js";
|
|
19
|
+
import type { PlanPart } from "../contracts/chat-parts.js";
|
|
20
|
+
import { type PlanRecord } from "../state/index.js";
|
|
19
21
|
export interface PlanCardData {
|
|
20
22
|
id: string;
|
|
21
23
|
planId: string;
|
|
24
|
+
parentPlanId?: string;
|
|
22
25
|
title?: string;
|
|
23
26
|
steps: unknown;
|
|
24
27
|
analytical_narrative: unknown;
|
|
@@ -37,6 +40,14 @@ export interface RunCardData {
|
|
|
37
40
|
title: string;
|
|
38
41
|
stepCount: number;
|
|
39
42
|
}
|
|
43
|
+
/** Build the `data-plan` payload of a stored plan record. `null` when the
|
|
44
|
+
* document no longer parses. An explicit `title` overrides the plan title. */
|
|
45
|
+
export declare function planCardOf(record: PlanRecord, title?: string): PlanCardData | null;
|
|
46
|
+
/** The `data-plan` part of a stored plan record, with the title of the plan. The
|
|
47
|
+
* part schema drops each key that it does not name, thus the part equals the
|
|
48
|
+
* stored card of a transcript. `null` when the document does not parse or the
|
|
49
|
+
* schema refuses the part. */
|
|
50
|
+
export declare function planPartOf(record: PlanRecord): PlanPart | null;
|
|
40
51
|
/** Build the `data-plan` payload by loading the stored plan. `ok(null)` when
|
|
41
52
|
* the plan is missing or no longer parses (absence is NOT an error) — the
|
|
42
53
|
* caller falls back to a chip. */
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { okAsync } from "neverthrow";
|
|
18
18
|
import { tryQuery } from "../lib/db-result.js";
|
|
19
|
+
import { PlanPartSchema } from "../contracts/schemas/chat-parts.js";
|
|
19
20
|
import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
|
|
20
|
-
import { loadPlan } from "../state/index.js";
|
|
21
|
+
import { loadPlan, loadPlanRecord } from "../state/index.js";
|
|
21
22
|
import { parseStructureUrl } from "../contracts/structure-source.js";
|
|
22
23
|
import { normalizeEchartSpec } from "../tools/display/normalize-echart-spec.js";
|
|
23
24
|
import { validatePath } from "../tools/lib/path-validation.js";
|
|
@@ -32,30 +33,43 @@ function presentationId(input) {
|
|
|
32
33
|
.slice(0, 16);
|
|
33
34
|
return `pres-${hash}`;
|
|
34
35
|
}
|
|
36
|
+
/** Build the `data-plan` payload of a stored plan record. `null` when the
|
|
37
|
+
* document no longer parses. An explicit `title` overrides the plan title. */
|
|
38
|
+
export function planCardOf(record, title) {
|
|
39
|
+
const parsed = AnalysisPlanSchema.safeParse(record.plan);
|
|
40
|
+
if (!parsed.success)
|
|
41
|
+
return null;
|
|
42
|
+
const id = `pres-${createHash("sha256").update(record.planId).digest("hex").slice(0, 16)}`;
|
|
43
|
+
// Explicit `show_plan({ title })` overrides; otherwise default to the
|
|
44
|
+
// planner-set plan title so both cards share one source.
|
|
45
|
+
const heading = title ?? parsed.data.title;
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
planId: record.planId,
|
|
49
|
+
...(record.parentPlanId !== null ? { parentPlanId: record.parentPlanId } : {}),
|
|
50
|
+
...(heading !== undefined ? { title: heading } : {}),
|
|
51
|
+
steps: parsed.data.steps,
|
|
52
|
+
analytical_narrative: parsed.data.analytical_narrative,
|
|
53
|
+
omicsType: parsed.data.omicsType,
|
|
54
|
+
omicsSubtype: parsed.data.omicsSubtype,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The `data-plan` part of a stored plan record, with the title of the plan. The
|
|
58
|
+
* part schema drops each key that it does not name, thus the part equals the
|
|
59
|
+
* stored card of a transcript. `null` when the document does not parse or the
|
|
60
|
+
* schema refuses the part. */
|
|
61
|
+
export function planPartOf(record) {
|
|
62
|
+
const card = planCardOf(record);
|
|
63
|
+
if (!card)
|
|
64
|
+
return null;
|
|
65
|
+
const part = PlanPartSchema.safeParse({ type: "data-plan", ...card });
|
|
66
|
+
return part.success ? part.data : null;
|
|
67
|
+
}
|
|
35
68
|
/** Build the `data-plan` payload by loading the stored plan. `ok(null)` when
|
|
36
69
|
* the plan is missing or no longer parses (absence is NOT an error) — the
|
|
37
70
|
* caller falls back to a chip. */
|
|
38
71
|
export function buildPlanCardData(pool, planId, analysisId, title) {
|
|
39
|
-
return
|
|
40
|
-
if (!plan)
|
|
41
|
-
return null;
|
|
42
|
-
const parsed = AnalysisPlanSchema.safeParse(plan);
|
|
43
|
-
if (!parsed.success)
|
|
44
|
-
return null;
|
|
45
|
-
const id = `pres-${createHash("sha256").update(planId).digest("hex").slice(0, 16)}`;
|
|
46
|
-
// Explicit `show_plan({ title })` overrides; otherwise default to the
|
|
47
|
-
// planner-set plan title so both cards share one source.
|
|
48
|
-
const heading = title ?? parsed.data.title;
|
|
49
|
-
return {
|
|
50
|
-
id,
|
|
51
|
-
planId,
|
|
52
|
-
...(heading !== undefined ? { title: heading } : {}),
|
|
53
|
-
steps: parsed.data.steps,
|
|
54
|
-
analytical_narrative: parsed.data.analytical_narrative,
|
|
55
|
-
omicsType: parsed.data.omicsType,
|
|
56
|
-
omicsSubtype: parsed.data.omicsSubtype,
|
|
57
|
-
};
|
|
58
|
-
});
|
|
72
|
+
return loadPlanRecord(pool, planId, { analysisId }).map((record) => (record ? planCardOf(record, title) : null));
|
|
59
73
|
}
|
|
60
74
|
/** Build the `data-run-card` payload for a started analysis run. Loads the
|
|
61
75
|
* plan (for `title` + `stepCount`) the same way `buildPlanCardData` does. The
|
|
@@ -18,8 +18,8 @@ export interface ConversationDisplayRecorderOptions {
|
|
|
18
18
|
/** The root agent of the turn. Its call path is `[agentId]`, and an event with a longer path is from a sub-agent. */
|
|
19
19
|
readonly agentId: string;
|
|
20
20
|
readonly sink: EmitFn;
|
|
21
|
-
|
|
22
|
-
readonly
|
|
21
|
+
/** The clock for the times of the sub-agent activity, in milliseconds. The default is the wall clock. */
|
|
22
|
+
readonly now?: () => number;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Record the display of a turn through the same translation path as a live surface: each event becomes a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { applyChatFrame, checkChatPart, isRootFrame, toChatFrame } from "../contracts/chat-frame.js";
|
|
3
3
|
import { PART_REGISTRY } from "../contracts/part-registry.js";
|
|
4
|
+
import { createSubAgentActivityTracker } from "../contracts/subagent-activity.js";
|
|
4
5
|
import { compactionExchangeOf, compactionMarkerOf } from "./ai-sdk-message-storage.js";
|
|
5
6
|
import { conversationDisplayPart } from "./conversation-display-storage.js";
|
|
6
7
|
function jsonCopy(value) {
|
|
@@ -63,12 +64,16 @@ function compactionDivider(marker) {
|
|
|
63
64
|
*/
|
|
64
65
|
export function createConversationDisplayRecorder(options) {
|
|
65
66
|
// One id for each message of the turn, thus the rounds of a turn replay as one assistant message.
|
|
66
|
-
const userMessageId =
|
|
67
|
-
let assistantMessageId =
|
|
67
|
+
const userMessageId = randomUUID();
|
|
68
|
+
let assistantMessageId = randomUUID();
|
|
69
|
+
let userAnnounced = false;
|
|
70
|
+
let announcedAssistantId;
|
|
68
71
|
const rootSource = { agentId: options.agentId, callPath: [options.agentId] };
|
|
69
72
|
// The messages of the open round. A later update of a part lands in the next round, and the replay replaces the earlier copy.
|
|
70
73
|
let round = [];
|
|
71
74
|
let finished = false;
|
|
75
|
+
const now = options.now ?? Date.now;
|
|
76
|
+
const subAgentActivity = createSubAgentActivityTracker();
|
|
72
77
|
function apply(frame) {
|
|
73
78
|
round = applyChatFrame(round, frame, assistantMessageId).messages;
|
|
74
79
|
}
|
|
@@ -84,7 +89,13 @@ export function createConversationDisplayRecorder(options) {
|
|
|
84
89
|
}
|
|
85
90
|
function record(event) {
|
|
86
91
|
const frame = toChatFrame(event, rootSource);
|
|
87
|
-
if (frame === null
|
|
92
|
+
if (frame === null)
|
|
93
|
+
return;
|
|
94
|
+
// The tracker reads the frames of the sub-agents, and `applyChatFrame` ignores them.
|
|
95
|
+
const activity = subAgentActivity.track(frame, now());
|
|
96
|
+
if (activity !== null && durableConversationType(activity.type))
|
|
97
|
+
apply(recordedPart(activity));
|
|
98
|
+
if (!isRootFrame(frame))
|
|
88
99
|
return;
|
|
89
100
|
if (frame.type.startsWith("data-")) {
|
|
90
101
|
// Each chat event type is named without the `data-` prefix, thus a `data-` frame is a part frame.
|
|
@@ -94,10 +105,33 @@ export function createConversationDisplayRecorder(options) {
|
|
|
94
105
|
}
|
|
95
106
|
apply(frame);
|
|
96
107
|
}
|
|
108
|
+
function messageStart(messageId, role) {
|
|
109
|
+
return { type: "message-start", messageId, role, source: rootSource };
|
|
110
|
+
}
|
|
111
|
+
/** The `message-start` of each id that the sink did not get yet. */
|
|
112
|
+
function announcements() {
|
|
113
|
+
const starts = [];
|
|
114
|
+
if (!userAnnounced) {
|
|
115
|
+
userAnnounced = true;
|
|
116
|
+
starts.push(messageStart(userMessageId, "user"));
|
|
117
|
+
}
|
|
118
|
+
if (announcedAssistantId !== assistantMessageId) {
|
|
119
|
+
announcedAssistantId = assistantMessageId;
|
|
120
|
+
starts.push(messageStart(assistantMessageId, "assistant"));
|
|
121
|
+
}
|
|
122
|
+
return starts;
|
|
123
|
+
}
|
|
124
|
+
// A `message-start` goes to the sink only immediately before an event: a host reads the first call of its sink
|
|
125
|
+
// as the start of the turn, and a turn that ends before any event must still get its plain refusal (for example an HTTP 402).
|
|
97
126
|
const emit = (event) => {
|
|
98
127
|
if (!finished)
|
|
99
128
|
record(event);
|
|
100
|
-
|
|
129
|
+
const starts = announcements();
|
|
130
|
+
if (starts.length === 0)
|
|
131
|
+
return options.sink(event);
|
|
132
|
+
// The calls start in the order of the stream with no await between them: a later emit that its caller does
|
|
133
|
+
// not await, for example a text delta, must not reach the sink before them.
|
|
134
|
+
return Promise.all([...starts, event].map((next) => options.sink(next))).then(() => undefined);
|
|
101
135
|
};
|
|
102
136
|
function takeOpening() {
|
|
103
137
|
return [{ id: userMessageId, role: "user", parts: [{ type: "text", text: options.userText, state: "done" }] }];
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* reconstruction.
|
|
16
16
|
*/
|
|
17
17
|
import { type DataUIPart, type UIMessage } from "ai";
|
|
18
|
-
import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart } from "../contracts/chat-parts.js";
|
|
18
|
+
import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
|
|
19
19
|
import type { ToolCallOutcome } from "../contracts/chat-events.js";
|
|
20
20
|
import type { ChatMessage, MessagePart, ToolCallPart } from "../contracts/message.js";
|
|
21
21
|
import type { Logger } from "../lib/logger.js";
|
|
@@ -37,11 +37,17 @@ export type ConversationUIData = {
|
|
|
37
37
|
presentation: Payload<PresentationPart>;
|
|
38
38
|
plan: Payload<PlanPart>;
|
|
39
39
|
"run-card": Payload<RunCardPart>;
|
|
40
|
+
"plan-approval": Payload<PlanApprovalPart>;
|
|
40
41
|
"file-reference": Payload<FileReferencePart>;
|
|
41
42
|
ask: Payload<AskPart>;
|
|
42
43
|
"child-session-started": Payload<ChildSessionStartedPart>;
|
|
43
44
|
"report-rendered": Payload<ReportRenderedPart>;
|
|
44
45
|
compaction: Payload<CompactionPart>;
|
|
46
|
+
"subagent-activity": Omit<Payload<SubAgentActivityPart>, "entries"> & {
|
|
47
|
+
entries: (Omit<SubAgentActivityEntry, "outcome"> & {
|
|
48
|
+
outcome: ToolCallOutcome;
|
|
49
|
+
})[];
|
|
50
|
+
};
|
|
45
51
|
};
|
|
46
52
|
export interface ConversationDisplayMetadata {
|
|
47
53
|
interrupted?: boolean;
|
|
@@ -63,7 +69,8 @@ export declare function parseStoredDisplayEnvelope(value: unknown, identity: str
|
|
|
63
69
|
* flight) and required here, because a record is of a call that is no longer
|
|
64
70
|
* running. An in-flight call being recorded is one the turn never saw finish, so
|
|
65
71
|
* absence resolves to `incomplete` at exactly this boundary — once, where the
|
|
66
|
-
* lifetime changes — rather than at each reader.
|
|
72
|
+
* lifetime changes — rather than at each reader. An entry of a sub-agent call
|
|
73
|
+
* closes the same way, and an `incomplete` entry has no end time.
|
|
67
74
|
*/
|
|
68
75
|
export declare function conversationDisplayPart(part: Exclude<MessagePart, {
|
|
69
76
|
type: "text";
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { validateUIMessages } from "ai";
|
|
18
18
|
import { z } from "zod";
|
|
19
|
-
import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
19
|
+
import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
20
20
|
import { ToolCallOutcomeSchema } from "../contracts/schemas/chat-events.js";
|
|
21
21
|
export const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
|
|
22
22
|
const DisplayEnvelopeHeaderSchema = z.object({
|
|
@@ -65,11 +65,15 @@ const dataSchemas = {
|
|
|
65
65
|
presentation: PresentationPartSchema.omit({ type: true }),
|
|
66
66
|
plan: PlanPartSchema.omit({ type: true }),
|
|
67
67
|
"run-card": RunCardPartSchema.omit({ type: true }),
|
|
68
|
+
"plan-approval": PlanApprovalPartSchema.omit({ type: true }),
|
|
68
69
|
"file-reference": FileReferencePartSchema.omit({ type: true }),
|
|
69
70
|
ask: AskPartSchema.omit({ type: true }),
|
|
70
71
|
"child-session-started": ChildSessionStartedPartSchema.omit({ type: true }),
|
|
71
72
|
"report-rendered": ReportRenderedPartSchema.omit({ type: true }),
|
|
72
73
|
compaction: CompactionPartSchema.omit({ type: true }),
|
|
74
|
+
"subagent-activity": SubAgentActivityPartSchema.omit({ type: true }).extend({
|
|
75
|
+
entries: z.array(SubAgentActivityEntrySchema.extend({ outcome: ToolCallOutcomeSchema })),
|
|
76
|
+
}),
|
|
73
77
|
};
|
|
74
78
|
export function envelopeDisplayMessages(messages) {
|
|
75
79
|
if (messages.length === 0)
|
|
@@ -157,6 +161,12 @@ export async function parseStoredDisplayEnvelope(value, identity, logger) {
|
|
|
157
161
|
function displayPartType(part) {
|
|
158
162
|
return (part.type === "tool-call" ? "data-tool-call" : part.type);
|
|
159
163
|
}
|
|
164
|
+
function closedEntry(entry) {
|
|
165
|
+
if (entry.outcome !== undefined)
|
|
166
|
+
return { ...entry, outcome: entry.outcome };
|
|
167
|
+
const { endedAt: _endedAt, ...open } = entry;
|
|
168
|
+
return { ...open, outcome: "incomplete" };
|
|
169
|
+
}
|
|
160
170
|
/** The id a part reconciles on: a tool call by its call id, a card by its own stable id. */
|
|
161
171
|
function displayPartId(part) {
|
|
162
172
|
if (part.type === "tool-call")
|
|
@@ -171,11 +181,16 @@ function displayPartId(part) {
|
|
|
171
181
|
* flight) and required here, because a record is of a call that is no longer
|
|
172
182
|
* running. An in-flight call being recorded is one the turn never saw finish, so
|
|
173
183
|
* absence resolves to `incomplete` at exactly this boundary — once, where the
|
|
174
|
-
* lifetime changes — rather than at each reader.
|
|
184
|
+
* lifetime changes — rather than at each reader. An entry of a sub-agent call
|
|
185
|
+
* closes the same way, and an `incomplete` entry has no end time.
|
|
175
186
|
*/
|
|
176
187
|
export function conversationDisplayPart(part) {
|
|
177
188
|
const { type: _type, ...rest } = part;
|
|
178
|
-
const data = part.type === "tool-call"
|
|
189
|
+
const data = part.type === "tool-call"
|
|
190
|
+
? { ...rest, outcome: part.outcome ?? "incomplete" }
|
|
191
|
+
: part.type === "data-subagent-activity"
|
|
192
|
+
? { ...rest, entries: part.entries.map(closedEntry) }
|
|
193
|
+
: rest;
|
|
179
194
|
return { type: displayPartType(part), id: displayPartId(part), data };
|
|
180
195
|
}
|
|
181
196
|
/**
|
|
@@ -27,8 +27,9 @@ import type { TokenUsageRollup } from "../contracts/usage.js";
|
|
|
27
27
|
import { type DbError } from "../lib/db-result.js";
|
|
28
28
|
import type { Logger } from "../lib/logger.js";
|
|
29
29
|
import { type StoredMessageEnvelope } from "./ai-sdk-message-storage.js";
|
|
30
|
+
import type { MessagePart } from "../contracts/message.js";
|
|
30
31
|
import { type ConversationUIMessage, type StoredDisplayEnvelope } from "./conversation-display-storage.js";
|
|
31
|
-
/** One stored message, as returned by the display
|
|
32
|
+
/** One stored message, as returned by the display reads (`loadAll` and `loadPage`). */
|
|
32
33
|
export interface StoredMessage {
|
|
33
34
|
readonly seq: number;
|
|
34
35
|
readonly envelope: StoredMessageEnvelope;
|
|
@@ -104,8 +105,14 @@ export interface ConversationTurn {
|
|
|
104
105
|
* never shown. The harness owns the record-to-`system` mapping for the same
|
|
105
106
|
* reason it owns the marker — a host that hand-assembled either could produce a
|
|
106
107
|
* message the turn-boundary predicates fail to recognise.
|
|
108
|
+
*
|
|
109
|
+
* With `displayParts`, the display message holds those parts in place of the
|
|
110
|
+
* text part, and the model message is still the record of `text`. The text then
|
|
111
|
+
* stays for the model alone.
|
|
107
112
|
*/
|
|
108
|
-
export declare function conversationRecordTurn(text: string
|
|
113
|
+
export declare function conversationRecordTurn(text: string, displayParts?: readonly Exclude<MessagePart, {
|
|
114
|
+
type: "text";
|
|
115
|
+
}>[]): ConversationTurn;
|
|
109
116
|
/** The status of a chat turn: `open` while it runs, then its outcome. */
|
|
110
117
|
export type TurnStatus = "open" | "done" | "aborted" | "failed";
|
|
111
118
|
/** The close of a chat turn. It sets the outcome on the turn record, one time. */
|
|
@@ -165,6 +172,20 @@ export interface LoadRecentOptions {
|
|
|
165
172
|
/** Keep the first turn of the thread, the seed of a report thread, in front of the latest summary. */
|
|
166
173
|
readonly keepFirstTurn?: boolean;
|
|
167
174
|
}
|
|
175
|
+
/** The options of {@link ThreadHistory.loadPage}. */
|
|
176
|
+
export interface LoadPageOptions {
|
|
177
|
+
/** The cursor that the previous page gave. Absent for the newest page. */
|
|
178
|
+
readonly before?: number;
|
|
179
|
+
/** The maximum count of turns in the page. */
|
|
180
|
+
readonly limit: number;
|
|
181
|
+
}
|
|
182
|
+
/** One page of the display read, as {@link ThreadHistory.loadPage} gives it. */
|
|
183
|
+
export interface TurnPage {
|
|
184
|
+
/** The whole turns of the page, oldest-first. */
|
|
185
|
+
readonly turns: StoredMessage[][];
|
|
186
|
+
/** The cursor of the next older page, or `null` when the page holds the oldest row of the thread. */
|
|
187
|
+
readonly before: number | null;
|
|
188
|
+
}
|
|
168
189
|
/**
|
|
169
190
|
* The conversation message store. Two methods, by design — no generic row
|
|
170
191
|
* insert (see the harness-thread-store spec). `threadId` is the conversation scope — one UI thread.
|
|
@@ -188,12 +209,8 @@ export interface ThreadHistory {
|
|
|
188
209
|
/**
|
|
189
210
|
* Return a thread's messages oldest-first for UI display, grouped into turns.
|
|
190
211
|
* It gives each row, and the view of the agent loop is `loadRecent`'s job.
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* selects and parses every row of the thread and would slice only afterwards,
|
|
194
|
-
* so a page saves no query and no parse. It would only truncate the answer,
|
|
195
|
-
* which is a way to lose turns and not a way to save work. A reader wanting a
|
|
196
|
-
* window takes one here, where it knows what it is bounding.
|
|
212
|
+
* It selects and parses every row of the thread, thus its work grows with the
|
|
213
|
+
* thread. {@link ThreadHistory.loadPage} bounds that work.
|
|
197
214
|
*
|
|
198
215
|
* Grouped rather than flat because the grouping is what the read computes and
|
|
199
216
|
* what a turn count means, and `.flat()` recovers the rows in `seq` order for
|
|
@@ -202,6 +219,23 @@ export interface ThreadHistory {
|
|
|
202
219
|
* this read already found.
|
|
203
220
|
*/
|
|
204
221
|
loadAll(threadId: string): ResultAsync<StoredMessage[][], DbError>;
|
|
222
|
+
/**
|
|
223
|
+
* Return the newest whole turns of a thread for UI display, up to `limit` turns, oldest-first.
|
|
224
|
+
* With `before`, it returns the newest whole turns below the turn that the cursor names.
|
|
225
|
+
* The rows are the same as the rows of {@link ThreadHistory.loadAll}.
|
|
226
|
+
*
|
|
227
|
+
* The read selects and parses only the rows of its page, thus its work does not grow with
|
|
228
|
+
* the turns before the page. The page edges inside the thread are turn starts, so the read
|
|
229
|
+
* never truncates a turn, also for a cursor that is not a turn start: the turn of the cursor
|
|
230
|
+
* row is then the newest turn of the page, and it is whole. The rows before the first turn
|
|
231
|
+
* start make one group, and the group counts as a turn.
|
|
232
|
+
*
|
|
233
|
+
* The cursor is the `seq` of the opening row of the oldest turn of the page. A `seq` never
|
|
234
|
+
* moves and a retract removes only the tail, thus a cursor stays correct across a later
|
|
235
|
+
* append and a later retract. A `limit` that is not finite reads as 1, and each other
|
|
236
|
+
* `limit` reads as its integer part with a minimum of 1.
|
|
237
|
+
*/
|
|
238
|
+
loadPage(threadId: string, options: LoadPageOptions): ResultAsync<TurnPage, DbError>;
|
|
205
239
|
/**
|
|
206
240
|
* Remove the thread's most recent turn — every row from the last
|
|
207
241
|
* genuine-user-start `seq` onward, and its turn record — in a single transaction.
|