@stigmer/runner 3.19.1-dev.20260920100552 → 3.21.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/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +7 -0
- package/dist/__test-utils__/execution-record-fixture.js +10 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/__test-utils__/hermetic-activity.d.ts +7 -4
- package/dist/__test-utils__/hermetic-activity.js +12 -2
- package/dist/__test-utils__/hermetic-activity.js.map +1 -1
- package/dist/__test-utils__/turn-input-fixture.js +0 -1
- package/dist/__test-utils__/turn-input-fixture.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.js +8 -3
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-settle.js +4 -1
- package/dist/activities/execute-cursor/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +1 -3
- package/dist/activities/execute-deep-agent/prompt-builder.js +2 -3
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/generate-session-subject.d.ts +2 -1
- package/dist/activities/generate-session-subject.js +18 -10
- package/dist/activities/generate-session-subject.js.map +1 -1
- package/dist/harness/run-turn.d.ts +3 -2
- package/dist/harness/run-turn.js +33 -6
- package/dist/harness/run-turn.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +26 -2
- package/dist/harness/transcript/builder.js +35 -2
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/turn-context.js +3 -1
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/harness/turn-timeline.d.ts +132 -0
- package/dist/harness/turn-timeline.js +213 -0
- package/dist/harness/turn-timeline.js.map +1 -0
- package/dist/shared/blueprint-resolver.d.ts +14 -4
- package/dist/shared/blueprint-resolver.js +23 -10
- package/dist/shared/blueprint-resolver.js.map +1 -1
- package/dist/shared/builtin-assistant-prompt.d.ts +30 -0
- package/dist/shared/builtin-assistant-prompt.js +36 -0
- package/dist/shared/builtin-assistant-prompt.js.map +1 -0
- package/dist/shared/cold-start-timing.d.ts +18 -2
- package/dist/shared/cold-start-timing.js +30 -3
- package/dist/shared/cold-start-timing.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/execution-record-fixture.ts +17 -1
- package/src/__test-utils__/hermetic-activity.ts +19 -6
- package/src/__test-utils__/timing-lines.ts +43 -0
- package/src/__test-utils__/turn-input-fixture.ts +0 -1
- package/src/activities/__tests__/generate-session-subject.test.ts +14 -0
- package/src/activities/execute-cursor/__tests__/goldens/prompt.enhanced.no-instructions.prompt.md +142 -0
- package/src/activities/execute-cursor/__tests__/hermetic/built-in-assistant.test.ts +127 -0
- package/src/activities/execute-cursor/__tests__/hermetic/turn-phases.test.ts +136 -0
- package/src/activities/execute-cursor/__tests__/prompt-goldens.test.ts +14 -1
- package/src/activities/execute-cursor/prompt-builder.ts +8 -3
- package/src/activities/execute-cursor/turn-settle.ts +4 -1
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.minimal.prompt.md +2 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/built-in-assistant.test.ts +101 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/turn-phases.test.ts +118 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +1 -1
- package/src/activities/execute-deep-agent/prompt-builder.ts +3 -5
- package/src/activities/generate-session-subject.ts +20 -13
- package/src/harness/README.md +4 -1
- package/src/harness/__tests__/run-turn.test.ts +53 -0
- package/src/harness/__tests__/turn-timeline.test.ts +274 -0
- package/src/harness/run-turn.ts +36 -6
- package/src/harness/transcript/__tests__/builder.test.ts +69 -1
- package/src/harness/transcript/builder.ts +39 -2
- package/src/harness/turn-context.ts +3 -1
- package/src/harness/turn-timeline.ts +248 -0
- package/src/shared/__tests__/blueprint-resolver.test.ts +78 -8
- package/src/shared/__tests__/cold-start-timing.test.ts +53 -5
- package/src/shared/blueprint-resolver.ts +25 -12
- package/src/shared/builtin-assistant-prompt.ts +38 -0
- package/src/shared/cold-start-timing.ts +29 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic pin: the BUILT-IN ASSISTANT through the whole `ExecuteDeepAgent`
|
|
3
|
+
* activity — a session that names no agent instance, so the blueprint chain
|
|
4
|
+
* stops at the session (`shared/blueprint-resolver.ts`), the control plane
|
|
5
|
+
* is never asked for an instance or an agent, and the system prompt opens
|
|
6
|
+
* with the one built-in text (`shared/builtin-assistant-prompt.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Invariant pinned: the agent-less record is a first-class turn, not an
|
|
9
|
+
* error arm — IN_PROGRESS then COMPLETED with the scripted answer, the same
|
|
10
|
+
* shape as `plain-turn.test.ts`; and the words the model saw are the shared
|
|
11
|
+
* ones, read off the scripted model's `systemPrompt` context. The status
|
|
12
|
+
* itself is not goldened here: nothing agent-shaped reaches the status, so
|
|
13
|
+
* the plain-turn golden already photographs it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
17
|
+
import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
18
|
+
|
|
19
|
+
vi.mock("../../../../shared/model-client.js", async () =>
|
|
20
|
+
(await import("../../__test-utils__/scripted-model-module.js")).scriptedModelClientModule(),
|
|
21
|
+
);
|
|
22
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
23
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
ScriptedClock,
|
|
28
|
+
createHermeticEnvironment,
|
|
29
|
+
type HermeticEnvironment,
|
|
30
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
31
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
32
|
+
import { BUILT_IN_ASSISTANT_INSTRUCTIONS } from "../../../../shared/builtin-assistant-prompt.js";
|
|
33
|
+
import {
|
|
34
|
+
beginDeepAgentScenario,
|
|
35
|
+
deepAgentExecutionRecord,
|
|
36
|
+
runDeepAgentTurn,
|
|
37
|
+
} from "../../__test-utils__/hermetic-deep-agent.js";
|
|
38
|
+
|
|
39
|
+
const USER_MESSAGE = "Say hello.";
|
|
40
|
+
const ASSISTANT_TEXT = "Hello from the built-in assistant.";
|
|
41
|
+
|
|
42
|
+
describe("ExecuteDeepAgent hermetic — the built-in assistant", () => {
|
|
43
|
+
let env: HermeticEnvironment;
|
|
44
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
45
|
+
const clock = new ScriptedClock();
|
|
46
|
+
|
|
47
|
+
beforeAll(() => {
|
|
48
|
+
env = createHermeticEnvironment();
|
|
49
|
+
registry = stubRegistryFetch();
|
|
50
|
+
clock.install();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
clock.uninstall();
|
|
55
|
+
registry.restore();
|
|
56
|
+
env.dispose();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("runs a session with no agent to COMPLETED on the built-in prompt, never reading an instance or an agent", async () => {
|
|
60
|
+
clock.reset();
|
|
61
|
+
const record = deepAgentExecutionRecord({ message: USER_MESSAGE, builtInAssistant: true });
|
|
62
|
+
const systemPrompts: string[] = [];
|
|
63
|
+
const instanceRead = vi.fn(async () => {
|
|
64
|
+
throw new Error("must not be reached");
|
|
65
|
+
});
|
|
66
|
+
const agentRead = vi.fn(async () => {
|
|
67
|
+
throw new Error("must not be reached");
|
|
68
|
+
});
|
|
69
|
+
const scenario = beginDeepAgentScenario({
|
|
70
|
+
env,
|
|
71
|
+
clock,
|
|
72
|
+
record,
|
|
73
|
+
clientOverrides: { getAgentInstance: instanceRead, getAgent: agentRead },
|
|
74
|
+
script: (_tools, context) => {
|
|
75
|
+
systemPrompts.push(context.systemPrompt);
|
|
76
|
+
return { turns: [{ text: ASSISTANT_TEXT, usage: { inputTokens: 900, outputTokens: 20 } }] };
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const invocation = await runDeepAgentTurn(scenario);
|
|
81
|
+
|
|
82
|
+
expect(invocation.outcome.kind).toBe("returned");
|
|
83
|
+
expect(record.persistedPhases).toEqual([
|
|
84
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
85
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
86
|
+
]);
|
|
87
|
+
const final = record.lastFullStatus!;
|
|
88
|
+
const ai = final.messages.filter((m) => m.type === MessageType.MESSAGE_AI);
|
|
89
|
+
expect(ai).toHaveLength(1);
|
|
90
|
+
expect(ai[0].content).toBe(ASSISTANT_TEXT);
|
|
91
|
+
|
|
92
|
+
// The chain stopped at the session: the refusing reads were never
|
|
93
|
+
// reached (they would have failed the resolve phase).
|
|
94
|
+
expect(instanceRead).not.toHaveBeenCalled();
|
|
95
|
+
expect(agentRead).not.toHaveBeenCalled();
|
|
96
|
+
|
|
97
|
+
// The model met the shared words, first.
|
|
98
|
+
expect(systemPrompts).toHaveLength(1);
|
|
99
|
+
expect(systemPrompts[0].startsWith(BUILT_IN_ASSISTANT_INSTRUCTIONS)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic proof: the native harness gets the runtime's `turn_phases` timing
|
|
3
|
+
* line without a line of its own.
|
|
4
|
+
*
|
|
5
|
+
* Invariant pinned: driving the REAL deepagents graph through the real native
|
|
6
|
+
* translator, the turn runtime writes exactly one `turn_phases` line for the
|
|
7
|
+
* turn, and the line reads what the translator's events said — one model
|
|
8
|
+
* round per LangGraph run (two here: the proposal and the closing text), one
|
|
9
|
+
* tool span for the `read_file` call, the three root instants present, the
|
|
10
|
+
* adapter's own name as `harness`. The fold and its rules are
|
|
11
|
+
* `harness/__tests__/turn-timeline.test.ts`'s; the runtime's act of writing
|
|
12
|
+
* the line is `harness/__tests__/run-turn.test.ts`'s against the fake. What
|
|
13
|
+
* only this file can prove is that the native translator emits the events the
|
|
14
|
+
* fold needs (`message_start`/`message_finish` per run, `tool_started`/
|
|
15
|
+
* `tool_finished` per call) in the order it needs them, on a real graph.
|
|
16
|
+
*
|
|
17
|
+
* Structural assertions only: the scripted clock makes every number
|
|
18
|
+
* deterministic, but pinning one would pin the clock's tick quantum, not a
|
|
19
|
+
* behaviour. The status goldens beside this file are untouched by design —
|
|
20
|
+
* the line is stdout, never the status — and their byte-identity under this
|
|
21
|
+
* change is proved by running them.
|
|
22
|
+
*
|
|
23
|
+
* Same scenario as `tool-call.test.ts`, so the two files read together.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
29
|
+
|
|
30
|
+
vi.mock("../../../../shared/model-client.js", async () =>
|
|
31
|
+
(await import("../../__test-utils__/scripted-model-module.js")).scriptedModelClientModule(),
|
|
32
|
+
);
|
|
33
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
34
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
ScriptedClock,
|
|
39
|
+
createHermeticEnvironment,
|
|
40
|
+
type HermeticEnvironment,
|
|
41
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
42
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
43
|
+
import { captureTimingLines } from "../../../../__test-utils__/timing-lines.js";
|
|
44
|
+
import { TURN_PHASES_EVENT } from "../../../../harness/turn-timeline.js";
|
|
45
|
+
import {
|
|
46
|
+
beginDeepAgentScenario,
|
|
47
|
+
deepAgentExecutionRecord,
|
|
48
|
+
runDeepAgentTurn,
|
|
49
|
+
sessionWorkspaceDir,
|
|
50
|
+
} from "../../__test-utils__/hermetic-deep-agent.js";
|
|
51
|
+
|
|
52
|
+
const CALL_ID = "call-hermetic-read-0001";
|
|
53
|
+
const USER_MESSAGE = "What is in README.md?";
|
|
54
|
+
const README = "# Hermetic\n\nA fixture readme.\n";
|
|
55
|
+
const ASSISTANT_TEXT = "README.md holds a one-line fixture description.";
|
|
56
|
+
|
|
57
|
+
describe("ExecuteDeepAgent hermetic — the runtime's turn_phases line", () => {
|
|
58
|
+
let env: HermeticEnvironment;
|
|
59
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
60
|
+
const clock = new ScriptedClock();
|
|
61
|
+
|
|
62
|
+
beforeAll(() => {
|
|
63
|
+
env = createHermeticEnvironment();
|
|
64
|
+
registry = stubRegistryFetch();
|
|
65
|
+
clock.install();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(() => {
|
|
69
|
+
clock.uninstall();
|
|
70
|
+
registry.restore();
|
|
71
|
+
env.dispose();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("writes one line for the turn, reading the translator's rounds and tool spans", async () => {
|
|
75
|
+
const record = deepAgentExecutionRecord({ message: USER_MESSAGE });
|
|
76
|
+
const scenario = beginDeepAgentScenario({
|
|
77
|
+
env,
|
|
78
|
+
clock,
|
|
79
|
+
record,
|
|
80
|
+
script: () => ({
|
|
81
|
+
turns: [
|
|
82
|
+
{
|
|
83
|
+
text: "Let me read it.",
|
|
84
|
+
toolCalls: [{ id: CALL_ID, name: "read_file", args: { file_path: "/README.md" } }],
|
|
85
|
+
usage: { inputTokens: 1_500, outputTokens: 60 },
|
|
86
|
+
},
|
|
87
|
+
{ text: ASSISTANT_TEXT, usage: { inputTokens: 1_700, outputTokens: 30 } },
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
const workspace = sessionWorkspaceDir(env);
|
|
92
|
+
mkdirSync(workspace, { recursive: true });
|
|
93
|
+
writeFileSync(join(workspace, "README.md"), README, "utf-8");
|
|
94
|
+
|
|
95
|
+
const lines = await captureTimingLines(TURN_PHASES_EVENT, async () => {
|
|
96
|
+
const invocation = await runDeepAgentTurn(scenario);
|
|
97
|
+
expect(invocation.outcome.kind).toBe("returned");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(lines, "one line per turn").toHaveLength(1);
|
|
101
|
+
const line = lines[0]!;
|
|
102
|
+
expect(line.harness, "the adapter's diagnostic name, the runtime's vocabulary").toBe("deep-agent");
|
|
103
|
+
expect(line.outcome).toBe("completed");
|
|
104
|
+
expect(line.execution_id).toBe(record.execution.metadata!.id);
|
|
105
|
+
expect(line.turn_seq).toBeTypeOf("number");
|
|
106
|
+
|
|
107
|
+
expect(line.rounds, "the proposing run and the closing run").toBe(2);
|
|
108
|
+
expect(line.tool_calls).toBe(1);
|
|
109
|
+
expect(line.sub_agents).toBe(0);
|
|
110
|
+
expect(line.segments.filter((s) => s.name === "model_round")).toHaveLength(2);
|
|
111
|
+
expect(line.segments.filter((s) => s.name === "tool:read_file")).toHaveLength(1);
|
|
112
|
+
|
|
113
|
+
expect(line.first_event_ms).toBeTypeOf("number");
|
|
114
|
+
expect(line.first_visible_token_ms).toBeTypeOf("number");
|
|
115
|
+
expect(line.first_text_ms).toBeTypeOf("number");
|
|
116
|
+
expect(line.total_ms).toBeGreaterThanOrEqual(line.first_text_ms as number);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* the filter fires, channel templates, referenced files, three input files
|
|
21
21
|
* with a rename and a download URL, the vision disclosure, all five standing
|
|
22
22
|
* sections); the below-threshold skills branch; plan mode; build-from-plan;
|
|
23
|
-
* and the minimal prompt (no instructions, so
|
|
23
|
+
* and the minimal prompt (no instructions, so the built-in assistant's). Plan
|
|
24
24
|
* mode and build-from-plan are mutually exclusive in production, so each has
|
|
25
25
|
* its own golden.
|
|
26
26
|
*
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type StandingSectionKind,
|
|
41
41
|
type VisionPromptInfo,
|
|
42
42
|
} from "../../shared/prompt-sections.js";
|
|
43
|
+
import { effectiveInstructions } from "../../shared/builtin-assistant-prompt.js";
|
|
43
44
|
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
44
45
|
import {
|
|
45
46
|
buildImplementPlanDirective,
|
|
@@ -115,11 +116,8 @@ directly over delegating. Only delegate when context isolation \
|
|
|
115
116
|
or parallelism genuinely helps the user.
|
|
116
117
|
`;
|
|
117
118
|
|
|
118
|
-
/** The agent's instructions when the blueprint carries none; the prompt is never empty (since #1096: was `setup.ts`'s alone). */
|
|
119
|
-
export const DEFAULT_INSTRUCTIONS = "You are a helpful AI assistant.";
|
|
120
|
-
|
|
121
119
|
export interface PromptBuilderInput {
|
|
122
|
-
/** The blueprint's instructions, raw; empty
|
|
120
|
+
/** The blueprint's instructions, raw; empty reads the built-in assistant's (shared/builtin-assistant-prompt.ts). */
|
|
123
121
|
instructions: string;
|
|
124
122
|
provisionResults: ProvisionResult[];
|
|
125
123
|
containerRoot: string;
|
|
@@ -233,7 +231,7 @@ const STANDING_SECTION_HEADINGS: Record<StandingSectionKind, string> = {
|
|
|
233
231
|
* sections. Pure function with no I/O.
|
|
234
232
|
*/
|
|
235
233
|
export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
236
|
-
let prompt = input.instructions
|
|
234
|
+
let prompt = effectiveInstructions(input.instructions);
|
|
237
235
|
|
|
238
236
|
const workspaceSection = buildWorkspacePromptSection(
|
|
239
237
|
input.provisionResults,
|
|
@@ -137,22 +137,26 @@ export async function generateSessionSubject(
|
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// The agent, when the conversation has one, lends the title its purpose;
|
|
141
|
+
// the built-in assistant (no agent on the session) is titled from the
|
|
142
|
+
// message alone rather than left "Untitled".
|
|
143
|
+
let agentName = "";
|
|
144
|
+
let agentDescription = "";
|
|
140
145
|
const agentId = await resolveAgentId(execution, session, client);
|
|
141
|
-
if (agentId
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
if (agentId !== "") {
|
|
147
|
+
const agent = await getOrSkip(
|
|
148
|
+
() => client.getAgent(agentId),
|
|
149
|
+
`agent not found: ${agentId}`,
|
|
150
|
+
);
|
|
151
|
+
if (agent === undefined) return;
|
|
152
|
+
agentName = agent.metadata?.name ?? "";
|
|
153
|
+
agentDescription = agent.spec?.description ?? "";
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
const agent = await getOrSkip(
|
|
147
|
-
() => client.getAgent(agentId),
|
|
148
|
-
`agent not found: ${agentId}`,
|
|
149
|
-
);
|
|
150
|
-
if (agent === undefined) return;
|
|
151
|
-
|
|
152
156
|
const subject = await generateTitle({
|
|
153
157
|
userMessage,
|
|
154
|
-
agentName
|
|
155
|
-
agentDescription
|
|
158
|
+
agentName,
|
|
159
|
+
agentDescription,
|
|
156
160
|
executionId,
|
|
157
161
|
options,
|
|
158
162
|
});
|
|
@@ -175,7 +179,8 @@ export async function generateSessionSubject(
|
|
|
175
179
|
/**
|
|
176
180
|
* The agent behind the execution: the direct agent_id when the execution
|
|
177
181
|
* carries one, else resolved through the session's agent-instance chain.
|
|
178
|
-
* Empty string when
|
|
182
|
+
* Empty string when there is none (the built-in assistant) or when the
|
|
183
|
+
* instance cannot be read.
|
|
179
184
|
*/
|
|
180
185
|
export async function resolveAgentId(
|
|
181
186
|
execution: AgentExecution,
|
|
@@ -264,7 +269,9 @@ export function buildUserPrompt(
|
|
|
264
269
|
agentDescription: string,
|
|
265
270
|
): string {
|
|
266
271
|
let prompt = `User's first message:\n"${userMessage}"\n\n`;
|
|
267
|
-
|
|
272
|
+
if (agentName !== "") {
|
|
273
|
+
prompt += `Agent: ${agentName}\n`;
|
|
274
|
+
}
|
|
268
275
|
if (agentDescription !== "") {
|
|
269
276
|
prompt += `Agent purpose: ${agentDescription}\n`;
|
|
270
277
|
}
|
package/src/harness/README.md
CHANGED
|
@@ -47,6 +47,10 @@ pinned somewhere. Read a row across before searching for anything.
|
|
|
47
47
|
translator emits.
|
|
48
48
|
- `harness/transcript/builder.ts` — `TranscriptBuilder`, the one builder every
|
|
49
49
|
harness folds its events through.
|
|
50
|
+
- `harness/turn-timeline.ts` — `TurnTimeline`, the runtime's fold over that
|
|
51
|
+
builder's events into the `turn_phases` timing line every harness gets for
|
|
52
|
+
free: first visible token, model rounds, tool and sub-agent spans, the
|
|
53
|
+
longest silence.
|
|
50
54
|
- `harness/capture.ts` — the file-review capture. The runtime owns all of it; an
|
|
51
55
|
adapter supplies one fact, which CAS-owned paths its engine touched.
|
|
52
56
|
- `harness/turn-context.ts` — the resolution phases that build the `TurnInput`
|
|
@@ -204,7 +208,6 @@ Every hit falls into one of three classes:
|
|
|
204
208
|
| `sdk/react/src/workflow/inspector/forms/AgentCallForm.tsx` | Two radio buttons, placed by hand. |
|
|
205
209
|
| `sdk/react/src/pricing-governance/BaselineEditor.tsx` | A select with one option per harness. |
|
|
206
210
|
| `site/src/components/pages/pricing/ModelPricingTable.tsx` | Harness to display label. |
|
|
207
|
-
| `site/src/data/harness-cost-comparison.json` | Per-harness cost rows. |
|
|
208
211
|
| `docs/concepts/harnesses.mdx`, `docs/concepts/sessions.mdx`, `docs/guides/runners/cursor-harness.mdx`, `docs/guides/workflows/task-types/agent-call.mdx`, `docs/vocabulary.md` | Hand-authored pages that name the harness set; each has an entry in `docs/_inventory/classification.yaml` to keep current. |
|
|
209
212
|
|
|
210
213
|
The cloud composition that serves api.stigmer.ai has sites of the same three
|
|
@@ -53,13 +53,16 @@ import {
|
|
|
53
53
|
assertFailedSurfaceWritesItsCopy,
|
|
54
54
|
assertNonExecutingDecisionSettlesSkipped,
|
|
55
55
|
assertRejectedBindFails,
|
|
56
|
+
assertWorkspaceLockTimeoutFails,
|
|
56
57
|
describeHarnessRuntimeContract,
|
|
57
58
|
RuntimeExecutionDriver,
|
|
58
59
|
slimOf,
|
|
59
60
|
systemMessages,
|
|
60
61
|
type RuntimeContractHarness,
|
|
61
62
|
} from "../../__test-utils__/harness-contract/runtime-contract.js";
|
|
63
|
+
import { captureTimingLines } from "../../__test-utils__/timing-lines.js";
|
|
62
64
|
import { TERMINAL_COPY } from "../terminal-table.js";
|
|
65
|
+
import { TURN_PHASES_EVENT } from "../turn-timeline.js";
|
|
63
66
|
import type { HarnessAdapter } from "../types.js";
|
|
64
67
|
|
|
65
68
|
const TASK_QUEUE = "runtime-test-queue";
|
|
@@ -219,6 +222,56 @@ describe("run-turn: the fake adapter through the real runtime", () => {
|
|
|
219
222
|
});
|
|
220
223
|
});
|
|
221
224
|
|
|
225
|
+
describe("the runtime's turn timeline", () => {
|
|
226
|
+
// `run-turn.ts` emits the `turn_phases` line once, after the adapter
|
|
227
|
+
// returns and the transcript is finalized, on every outcome; a turn that
|
|
228
|
+
// never reached the engine emits none. The line's CONTENT is
|
|
229
|
+
// `turn-timeline.test.ts`'s; what is pinned here is the runtime's act:
|
|
230
|
+
// that the fold is installed on the one builder the adapter is handed,
|
|
231
|
+
// that the line names the adapter and the activity's turn, and that it is
|
|
232
|
+
// written for a failed turn as for a completed one.
|
|
233
|
+
const turnPhasesLines = (act: () => Promise<void>) => captureTimingLines(TURN_PHASES_EVENT, act);
|
|
234
|
+
|
|
235
|
+
it("writes one turn_phases line for a completed turn, naming the adapter, the turn and what the fold saw", async () => {
|
|
236
|
+
clock.reset();
|
|
237
|
+
const driver = new RuntimeExecutionDriver(harness, "fake-timeline-completed");
|
|
238
|
+
const lines = await turnPhasesLines(async () => {
|
|
239
|
+
const invocation = await driver.turn([scenario.say("Reading it."), scenario.read("t1", "notes.md"), scenario.say("Done.")]);
|
|
240
|
+
expect(slimOf(subject, invocation).phase).toBe("EXECUTION_COMPLETED");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(lines).toHaveLength(1);
|
|
244
|
+
const [line] = lines;
|
|
245
|
+
expect(line!.harness).toBe(subject.adapter.name);
|
|
246
|
+
expect(line!.outcome).toBe("completed");
|
|
247
|
+
expect(line!.turn_seq).toBeTypeOf("number");
|
|
248
|
+
expect(line!.rounds, "two `say`s, two rounds").toBe(2);
|
|
249
|
+
expect(line!.tool_calls, "one `read`").toBe(1);
|
|
250
|
+
expect(line!.first_text_ms).toBeTypeOf("number");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("writes the line for a failed turn too: every outcome is measured", async () => {
|
|
254
|
+
clock.reset();
|
|
255
|
+
const driver = new RuntimeExecutionDriver(harness, "fake-timeline-failed");
|
|
256
|
+
const lines = await turnPhasesLines(async () => {
|
|
257
|
+
const invocation = await driver.turn([scenario.say("Checking."), scenario.fail("engine gave up")]);
|
|
258
|
+
expect(slimOf(subject, invocation).phase).toBe("EXECUTION_FAILED");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(lines).toHaveLength(1);
|
|
262
|
+
expect(lines[0]!.outcome).toBe("failed");
|
|
263
|
+
expect(lines[0]!.rounds).toBe(1);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("writes no line for a turn that settled during resolution: no engine, no phases", async () => {
|
|
267
|
+
clock.reset();
|
|
268
|
+
const lines = await turnPhasesLines(async () => {
|
|
269
|
+
await assertWorkspaceLockTimeoutFails(harness, "fake-timeline-no-engine");
|
|
270
|
+
});
|
|
271
|
+
expect(lines).toEqual([]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
222
275
|
describe("the runtime's write-back rule", () => {
|
|
223
276
|
// `run-turn.ts` calls `frame.writeback.finalize()` in exactly two places:
|
|
224
277
|
// once when a completed turn reaches its epilogue, and once when a resumed
|