@stigmer/runner 3.19.0 → 3.20.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/activities/execute-cursor/turn-settle.js +4 -1
- package/dist/activities/execute-cursor/turn-settle.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-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/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__/timing-lines.ts +43 -0
- package/src/activities/execute-cursor/__tests__/hermetic/turn-phases.test.ts +136 -0
- package/src/activities/execute-cursor/turn-settle.ts +4 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/turn-phases.test.ts +118 -0
- 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-timeline.ts +248 -0
- package/src/shared/__tests__/cold-start-timing.test.ts +53 -5
- package/src/shared/cold-start-timing.ts +29 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn runtime's timeline of one engine turn, folded from the transcript
|
|
3
|
+
* events every harness already emits, and written once at settle as the
|
|
4
|
+
* `turn_phases` `stigmer_timing` line.
|
|
5
|
+
*
|
|
6
|
+
* What it answers, for every harness alike and for any harness added later
|
|
7
|
+
* without a line in an adapter: when the user first saw something happen,
|
|
8
|
+
* how many model rounds and tool calls the turn took, where the sub-agents
|
|
9
|
+
* ran, and where the turn's longest silence was. It is the per-turn,
|
|
10
|
+
* CLIENT-PERCEIVED measurement from the runner's vantage, from the turn's
|
|
11
|
+
* origin (the activity's start, the same origin `execution_setup` counts
|
|
12
|
+
* from, so the two lines subtract without a join) to the adapter's return;
|
|
13
|
+
* the completion epilogue and the terminal persist that follow are the
|
|
14
|
+
* client's own end-to-end, read at `subscribe`. Its counterpart is the cloud
|
|
15
|
+
* proxy's `ProxyTiming` (`agentexecution/v1/usage.proto`): per model CALL,
|
|
16
|
+
* server-side, what the provider did. Neither restates the other; a report
|
|
17
|
+
* that wants both joins them by `execution_id`.
|
|
18
|
+
*
|
|
19
|
+
* Why a fold over the transcript events and not a mark in each adapter: the
|
|
20
|
+
* transcript builder (`transcript/builder.ts`) is the one view the runtime
|
|
21
|
+
* has of every harness's turn — model rounds, text, tools, sub-agents all
|
|
22
|
+
* reach it as a translator's output on one union — so a fold over it is
|
|
23
|
+
* written once and holds for the Cursor adapter, the native adapter and the
|
|
24
|
+
* next one. `UsageAccumulator` (`usage-accumulator.ts`) is the same shape
|
|
25
|
+
* for the usage deltas; this is that shape for the events.
|
|
26
|
+
*
|
|
27
|
+
* The fold's rules, each with an arm in `__tests__/turn-timeline.test.ts`:
|
|
28
|
+
*
|
|
29
|
+
* - Three instants, ROOT scope only (a sub-agent's text is not what the
|
|
30
|
+
* user is watching): `first_event_ms`, the first event of any kind;
|
|
31
|
+
* `first_visible_token_ms`, the first `text_delta` or `reasoning_delta`
|
|
32
|
+
* (both stream into the console as tokens); `first_text_ms`, the first
|
|
33
|
+
* `text_delta`. The two token instants are the cross-harness axes: with
|
|
34
|
+
* extended thinking on, the visible instant moves earlier and the text
|
|
35
|
+
* instant later, and a comparison needs both. `first_event_ms` is
|
|
36
|
+
* harness-relative — on native the first event is `message_start`, the
|
|
37
|
+
* model stream opening; on Cursor it is whatever the SDK delivered first —
|
|
38
|
+
* and is kept as a forensic (engine-ready to first model event), never as
|
|
39
|
+
* an axis. An instant that never happened is `null`.
|
|
40
|
+
* - One `model_round` span per `runId`, `message_start` to `message_finish`.
|
|
41
|
+
* What a `runId` is belongs to the translator: LangGraph's LLM-call run on
|
|
42
|
+
* native, one model round; a minted segment `<run>:<n>` on Cursor, a
|
|
43
|
+
* stretch of assistant output between tool calls, which the SDK does not
|
|
44
|
+
* let us tell apart from a provider round. The line's `rounds` reads the
|
|
45
|
+
* same way.
|
|
46
|
+
* - One `tool:<name>` span per `callId` (`tool:<slug>/<name>` for an MCP
|
|
47
|
+
* tool, so the MCP phase the runner skill's law asks for is legible),
|
|
48
|
+
* `tool_started` to the FIRST `tool_finished` or `tool_error`; a later
|
|
49
|
+
* fact for a settled span changes nothing, the builder's own upsert rule
|
|
50
|
+
* (Cursor completes every call twice). `approval_proposed` opens nothing:
|
|
51
|
+
* a call the engine held before it ran is not a tool phase.
|
|
52
|
+
* - One `sub_agent:<name>` span per `subAgentId`, opened by
|
|
53
|
+
* `sub_agent_started`, closed by `finished` or `failed`.
|
|
54
|
+
* - A fact that carries `observedAt` is placed at that instant, everything
|
|
55
|
+
* else at the fold's clock — the builder's rule for `completedAt`
|
|
56
|
+
* (`transcript/events.ts`, `Observed`), restated here because the
|
|
57
|
+
* alternative repeats the error #1097 fixed, for tool phases and on Cursor
|
|
58
|
+
* alone: a completion the SDK's delta channel reported is folded one
|
|
59
|
+
* stream event later, seconds while the model narrates, and a comparison
|
|
60
|
+
* that read the fold's clock would charge those seconds to Cursor's tools.
|
|
61
|
+
* Placing an observed instant needs a wall-clock origin beside the
|
|
62
|
+
* `performance.now()` one; the runtime captures both together.
|
|
63
|
+
* - `max_gap_ms`, the longest wait between two consecutive events, whatever
|
|
64
|
+
* their kind: the turn's longest silence as the user experienced it.
|
|
65
|
+
* `null` under two events.
|
|
66
|
+
* - A span still open at settle closes at settle: an interrupted turn's
|
|
67
|
+
* spans end where the turn did.
|
|
68
|
+
*
|
|
69
|
+
* The line: `emitTimingLog("turn_phases", context, recorder)` over a
|
|
70
|
+
* `TimingRecorder` on the turn's origin, every span appended in start order
|
|
71
|
+
* through `span()`. This is the first `stigmer_timing` event whose segments
|
|
72
|
+
* OVERLAP (a tool inside a round, a sub-agent across rounds); the recorder's
|
|
73
|
+
* `total_ms` is the latest end, not a sum, and a reader that summed the
|
|
74
|
+
* other timelines' segments must not sum this one's. No histogram mirrors it
|
|
75
|
+
* yet: the benchmark report is its reader and reads the log; an aggregate
|
|
76
|
+
* earns its instrument when a dashboard asks.
|
|
77
|
+
*
|
|
78
|
+
* Failure posture, the timing module's: the fold is arithmetic over the
|
|
79
|
+
* event and cannot affect a turn; `emit` is best-effort through
|
|
80
|
+
* `emitTimingLog`. Nothing here is persisted, and nothing branches on it.
|
|
81
|
+
*/
|
|
82
|
+
import { TimingRecorder, emitTimingLog } from "../shared/cold-start-timing.js";
|
|
83
|
+
/** The `stigmer_timing` selector of the line this module emits. */
|
|
84
|
+
export const TURN_PHASES_EVENT = "turn_phases";
|
|
85
|
+
export class TurnTimeline {
|
|
86
|
+
clock;
|
|
87
|
+
firstEventMs;
|
|
88
|
+
firstVisibleTokenMs;
|
|
89
|
+
firstTextMs;
|
|
90
|
+
lastEventMs;
|
|
91
|
+
maxGapMs;
|
|
92
|
+
rounds = new Map();
|
|
93
|
+
tools = new Map();
|
|
94
|
+
subAgents = new Map();
|
|
95
|
+
constructor(clock) {
|
|
96
|
+
this.clock = clock;
|
|
97
|
+
}
|
|
98
|
+
/** The builder's observer: place one folded event on the timeline. */
|
|
99
|
+
observe(event) {
|
|
100
|
+
const at = this.clock.now();
|
|
101
|
+
this.noteEvent(at);
|
|
102
|
+
// Root scope only: a sub-agent's tokens stream into its own row, not the
|
|
103
|
+
// thread the user is watching.
|
|
104
|
+
if ((event.kind === "text_delta" || event.kind === "reasoning_delta") && event.subAgentId === undefined) {
|
|
105
|
+
this.firstVisibleTokenMs ??= at;
|
|
106
|
+
if (event.kind === "text_delta")
|
|
107
|
+
this.firstTextMs ??= at;
|
|
108
|
+
}
|
|
109
|
+
switch (event.kind) {
|
|
110
|
+
case "message_start":
|
|
111
|
+
this.open(this.rounds, event.runId, "model_round", at);
|
|
112
|
+
return;
|
|
113
|
+
case "message_finish":
|
|
114
|
+
this.close(this.rounds, event.runId, at);
|
|
115
|
+
return;
|
|
116
|
+
case "tool_started":
|
|
117
|
+
this.open(this.tools, event.callId, toolSpanName(event.name, event.mcpServerSlug), at);
|
|
118
|
+
return;
|
|
119
|
+
case "tool_finished":
|
|
120
|
+
case "tool_error":
|
|
121
|
+
this.close(this.tools, event.callId, this.placeObserved(event.observedAt, at));
|
|
122
|
+
return;
|
|
123
|
+
case "sub_agent_started":
|
|
124
|
+
this.open(this.subAgents, event.subAgentId, `sub_agent:${event.name}`, at);
|
|
125
|
+
return;
|
|
126
|
+
case "sub_agent_finished":
|
|
127
|
+
case "sub_agent_failed":
|
|
128
|
+
this.close(this.subAgents, event.subAgentId, at);
|
|
129
|
+
return;
|
|
130
|
+
case "text_delta":
|
|
131
|
+
case "reasoning_delta":
|
|
132
|
+
case "tool_arg_delta":
|
|
133
|
+
case "tool_output_delta":
|
|
134
|
+
case "approval_proposed":
|
|
135
|
+
case "system_note":
|
|
136
|
+
return;
|
|
137
|
+
default: {
|
|
138
|
+
const exhaustive = event;
|
|
139
|
+
throw new Error(`unknown transcript event ${String(exhaustive.kind)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Write the line, once, at settle. Spans still open close now; the
|
|
145
|
+
* runtime's context (`execution_id`, `session_id`, `turn_seq`, `harness`,
|
|
146
|
+
* `outcome`) rides beside the fold's facts.
|
|
147
|
+
*/
|
|
148
|
+
emit(context) {
|
|
149
|
+
const settledAt = this.clock.now();
|
|
150
|
+
const recorder = new TimingRecorder(this.clock.originMs);
|
|
151
|
+
const spans = [...this.rounds.values(), ...this.tools.values(), ...this.subAgents.values()]
|
|
152
|
+
.sort((a, b) => a.startMs - b.startMs);
|
|
153
|
+
for (const span of spans) {
|
|
154
|
+
recorder.span(span.name, span.startMs, span.endMs ?? settledAt);
|
|
155
|
+
}
|
|
156
|
+
emitTimingLog(TURN_PHASES_EVENT, {
|
|
157
|
+
...context,
|
|
158
|
+
first_event_ms: this.offset(this.firstEventMs),
|
|
159
|
+
first_visible_token_ms: this.offset(this.firstVisibleTokenMs),
|
|
160
|
+
first_text_ms: this.offset(this.firstTextMs),
|
|
161
|
+
rounds: this.rounds.size,
|
|
162
|
+
tool_calls: this.tools.size,
|
|
163
|
+
sub_agents: this.subAgents.size,
|
|
164
|
+
max_gap_ms: this.maxGapMs === undefined ? null : round1(this.maxGapMs),
|
|
165
|
+
}, recorder);
|
|
166
|
+
}
|
|
167
|
+
noteEvent(at) {
|
|
168
|
+
this.firstEventMs ??= at;
|
|
169
|
+
if (this.lastEventMs !== undefined) {
|
|
170
|
+
const gap = at - this.lastEventMs;
|
|
171
|
+
if (this.maxGapMs === undefined || gap > this.maxGapMs)
|
|
172
|
+
this.maxGapMs = gap;
|
|
173
|
+
}
|
|
174
|
+
this.lastEventMs = at;
|
|
175
|
+
}
|
|
176
|
+
/** A span per identity: a re-emitted start (a resumed seeded call) keeps the first start. */
|
|
177
|
+
open(spans, id, name, at) {
|
|
178
|
+
if (spans.has(id))
|
|
179
|
+
return;
|
|
180
|
+
spans.set(id, { name, startMs: at, endMs: undefined });
|
|
181
|
+
}
|
|
182
|
+
/** The FIRST close settles a span; a later fact for it changes nothing. A close for an unknown id is ignored. */
|
|
183
|
+
close(spans, id, at) {
|
|
184
|
+
const span = spans.get(id);
|
|
185
|
+
if (!span || span.endMs !== undefined)
|
|
186
|
+
return;
|
|
187
|
+
span.endMs = at;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Where a fact goes on the `performance.now()` scale: its observed instant
|
|
191
|
+
* translated through the paired origins when it carries one, else the fold's
|
|
192
|
+
* clock. An unparseable stamp falls back to the fold's clock rather than
|
|
193
|
+
* losing the fact.
|
|
194
|
+
*/
|
|
195
|
+
placeObserved(observedAt, foldAt) {
|
|
196
|
+
if (observedAt === undefined)
|
|
197
|
+
return foldAt;
|
|
198
|
+
const wallMs = Date.parse(observedAt);
|
|
199
|
+
if (Number.isNaN(wallMs))
|
|
200
|
+
return foldAt;
|
|
201
|
+
return this.clock.originMs + (wallMs - this.clock.originWallMs);
|
|
202
|
+
}
|
|
203
|
+
offset(absoluteMs) {
|
|
204
|
+
return absoluteMs === undefined ? null : round1(absoluteMs - this.clock.originMs);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function toolSpanName(name, mcpServerSlug) {
|
|
208
|
+
return mcpServerSlug ? `tool:${mcpServerSlug}/${name}` : `tool:${name}`;
|
|
209
|
+
}
|
|
210
|
+
function round1(ms) {
|
|
211
|
+
return Math.round(ms * 10) / 10;
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=turn-timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-timeline.js","sourceRoot":"","sources":["../../src/harness/turn-timeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AAEH,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAG/E,mEAAmE;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAC;AA0B/C,MAAM,OAAO,YAAY;IACN,KAAK,CAAoB;IAElC,YAAY,CAAqB;IACjC,mBAAmB,CAAqB;IACxC,WAAW,CAAqB;IAChC,WAAW,CAAqB;IAChC,QAAQ,CAAqB;IAEpB,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IACrC,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAC;IACpC,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;IAEzD,YAAY,KAAwB;QAClC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,sEAAsE;IACtE,OAAO,CAAC,KAAsB;QAC5B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAEnB,yEAAyE;QACzE,+BAA+B;QAC/B,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,CAAC,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YACxG,IAAI,CAAC,mBAAmB,KAAK,EAAE,CAAC;YAChC,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;gBAAE,IAAI,CAAC,WAAW,KAAK,EAAE,CAAC;QAC3D,CAAC;QAED,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,eAAe;gBAClB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;gBACvD,OAAO;YACT,KAAK,gBAAgB;gBACnB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACzC,OAAO;YACT,KAAK,cAAc;gBACjB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC;gBACvF,OAAO;YACT,KAAK,eAAe,CAAC;YACrB,KAAK,YAAY;gBACf,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC;gBAC/E,OAAO;YACT,KAAK,mBAAmB;gBACtB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,aAAa,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;gBAC3E,OAAO;YACT,KAAK,oBAAoB,CAAC;YAC1B,KAAK,kBAAkB;gBACrB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;gBACjD,OAAO;YACT,KAAK,YAAY,CAAC;YAClB,KAAK,iBAAiB,CAAC;YACvB,KAAK,gBAAgB,CAAC;YACtB,KAAK,mBAAmB,CAAC;YACzB,KAAK,mBAAmB,CAAC;YACzB,KAAK,aAAa;gBAChB,OAAO;YACT,OAAO,CAAC,CAAC,CAAC;gBACR,MAAM,UAAU,GAAU,KAAK,CAAC;gBAChC,MAAM,IAAI,KAAK,CAAC,4BAA4B,MAAM,CAAE,UAA+B,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC/F,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,IAAI,CAAC,OAA0B;QAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;aACxF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;QACzC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC;QAClE,CAAC;QACD,aAAa,CAAC,iBAAiB,EAAE;YAC/B,GAAG,OAAO;YACV,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;YAC9C,sBAAsB,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC;YAC7D,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;YAC5C,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;YACxB,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI;YAC3B,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI;YAC/B,UAAU,EAAE,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;SACvE,EAAE,QAAQ,CAAC,CAAC;IACf,CAAC;IAEO,SAAS,CAAC,EAAU;QAC1B,IAAI,CAAC,YAAY,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,GAAG,GAAG,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC;YAClC,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,GAAG,GAAG,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;QAC9E,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;IACxB,CAAC;IAED,6FAA6F;IACrF,IAAI,CAAC,KAA4B,EAAE,EAAU,EAAE,IAAY,EAAE,EAAU;QAC7E,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO;QAC1B,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,iHAAiH;IACzG,KAAK,CAAC,KAA4B,EAAE,EAAU,EAAE,EAAU;QAChE,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO;QAC9C,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;IAClB,CAAC;IAED;;;;;OAKG;IACK,aAAa,CAAC,UAA8B,EAAE,MAAc;QAClE,IAAI,UAAU,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACtC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,OAAO,MAAM,CAAC;QACxC,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAClE,CAAC;IAEO,MAAM,CAAC,UAA8B;QAC3C,OAAO,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACpF,CAAC;CACF;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,aAAqB;IACvD,OAAO,aAAa,CAAC,CAAC,CAAC,QAAQ,aAAa,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,MAAM,CAAC,EAAU;IACxB,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;AAClC,CAAC"}
|
|
@@ -24,7 +24,7 @@ export interface TimingSegment {
|
|
|
24
24
|
readonly durationMs: number;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Records
|
|
27
|
+
* Records named segments on one timeline, two ways.
|
|
28
28
|
*
|
|
29
29
|
* `mark(name)` closes the span from the previous mark (or the origin) and
|
|
30
30
|
* names it — so instrumentation reads as one line placed AFTER the work it
|
|
@@ -35,11 +35,22 @@ export interface TimingSegment {
|
|
|
35
35
|
* const execution = await client.getExecution(executionId);
|
|
36
36
|
* timing.mark("fetch_execution");
|
|
37
37
|
* ```
|
|
38
|
+
*
|
|
39
|
+
* `span(name, startMs, endMs)` records a segment with explicit bounds, for a
|
|
40
|
+
* timeline whose phases OVERLAP (a tool call inside a model round, a
|
|
41
|
+
* sub-agent across two rounds) and so cannot be written as a chain of marks.
|
|
42
|
+
* A span never moves the mark cursor: a `mark` after a `span` still measures
|
|
43
|
+
* from the previous MARK, so the two idioms compose on one recorder without
|
|
44
|
+
* either changing what the other means. `totalMs()` is the latest end on the
|
|
45
|
+
* timeline, whichever way it was recorded — for a mark-only recorder that is
|
|
46
|
+
* the last mark, as it always was.
|
|
38
47
|
*/
|
|
39
48
|
export declare class TimingRecorder {
|
|
40
49
|
private readonly originMs;
|
|
41
50
|
private readonly segments;
|
|
42
51
|
private lastMarkMs;
|
|
52
|
+
/** Absolute end of the segment that ends latest, by either idiom. */
|
|
53
|
+
private latestEndMs;
|
|
43
54
|
/**
|
|
44
55
|
* @param originMs absolute `performance.now()`-scale origin. Defaults to
|
|
45
56
|
* "now" (per-execution timelines). Pass 0 for a process-lifetime timeline
|
|
@@ -49,7 +60,12 @@ export declare class TimingRecorder {
|
|
|
49
60
|
constructor(originMs?: number);
|
|
50
61
|
/** Close the span since the previous mark (or origin) and name it. */
|
|
51
62
|
mark(name: string): void;
|
|
52
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Record a segment with explicit absolute bounds (`performance.now()`
|
|
65
|
+
* scale, like the origin). Leaves the mark cursor where it was.
|
|
66
|
+
*/
|
|
67
|
+
span(name: string, startMs: number, endMs: number): void;
|
|
68
|
+
/** Elapsed time from the origin to the latest end on the timeline (ms). */
|
|
53
69
|
totalMs(): number;
|
|
54
70
|
snapshot(): readonly TimingSegment[];
|
|
55
71
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// while per-call dynamic imports raced under concurrent emissions.
|
|
22
22
|
import { getInstruments } from "../otel-metrics.js";
|
|
23
23
|
/**
|
|
24
|
-
* Records
|
|
24
|
+
* Records named segments on one timeline, two ways.
|
|
25
25
|
*
|
|
26
26
|
* `mark(name)` closes the span from the previous mark (or the origin) and
|
|
27
27
|
* names it — so instrumentation reads as one line placed AFTER the work it
|
|
@@ -32,11 +32,22 @@ import { getInstruments } from "../otel-metrics.js";
|
|
|
32
32
|
* const execution = await client.getExecution(executionId);
|
|
33
33
|
* timing.mark("fetch_execution");
|
|
34
34
|
* ```
|
|
35
|
+
*
|
|
36
|
+
* `span(name, startMs, endMs)` records a segment with explicit bounds, for a
|
|
37
|
+
* timeline whose phases OVERLAP (a tool call inside a model round, a
|
|
38
|
+
* sub-agent across two rounds) and so cannot be written as a chain of marks.
|
|
39
|
+
* A span never moves the mark cursor: a `mark` after a `span` still measures
|
|
40
|
+
* from the previous MARK, so the two idioms compose on one recorder without
|
|
41
|
+
* either changing what the other means. `totalMs()` is the latest end on the
|
|
42
|
+
* timeline, whichever way it was recorded — for a mark-only recorder that is
|
|
43
|
+
* the last mark, as it always was.
|
|
35
44
|
*/
|
|
36
45
|
export class TimingRecorder {
|
|
37
46
|
originMs;
|
|
38
47
|
segments = [];
|
|
39
48
|
lastMarkMs;
|
|
49
|
+
/** Absolute end of the segment that ends latest, by either idiom. */
|
|
50
|
+
latestEndMs;
|
|
40
51
|
/**
|
|
41
52
|
* @param originMs absolute `performance.now()`-scale origin. Defaults to
|
|
42
53
|
* "now" (per-execution timelines). Pass 0 for a process-lifetime timeline
|
|
@@ -46,6 +57,7 @@ export class TimingRecorder {
|
|
|
46
57
|
constructor(originMs = performance.now()) {
|
|
47
58
|
this.originMs = originMs;
|
|
48
59
|
this.lastMarkMs = originMs;
|
|
60
|
+
this.latestEndMs = originMs;
|
|
49
61
|
}
|
|
50
62
|
/** Close the span since the previous mark (or origin) and name it. */
|
|
51
63
|
mark(name) {
|
|
@@ -56,10 +68,25 @@ export class TimingRecorder {
|
|
|
56
68
|
durationMs: round1(now - this.lastMarkMs),
|
|
57
69
|
});
|
|
58
70
|
this.lastMarkMs = now;
|
|
71
|
+
if (now > this.latestEndMs)
|
|
72
|
+
this.latestEndMs = now;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Record a segment with explicit absolute bounds (`performance.now()`
|
|
76
|
+
* scale, like the origin). Leaves the mark cursor where it was.
|
|
77
|
+
*/
|
|
78
|
+
span(name, startMs, endMs) {
|
|
79
|
+
this.segments.push({
|
|
80
|
+
name,
|
|
81
|
+
startMs: round1(startMs - this.originMs),
|
|
82
|
+
durationMs: round1(endMs - startMs),
|
|
83
|
+
});
|
|
84
|
+
if (endMs > this.latestEndMs)
|
|
85
|
+
this.latestEndMs = endMs;
|
|
59
86
|
}
|
|
60
|
-
/**
|
|
87
|
+
/** Elapsed time from the origin to the latest end on the timeline (ms). */
|
|
61
88
|
totalMs() {
|
|
62
|
-
return round1(this.
|
|
89
|
+
return round1(this.latestEndMs - this.originMs);
|
|
63
90
|
}
|
|
64
91
|
snapshot() {
|
|
65
92
|
return this.segments;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cold-start-timing.js","sourceRoot":"","sources":["../../src/shared/cold-start-timing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,4EAA4E;AAC5E,2EAA2E;AAC3E,mEAAmE;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAUpD
|
|
1
|
+
{"version":3,"file":"cold-start-timing.js","sourceRoot":"","sources":["../../src/shared/cold-start-timing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/runner",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"description": "Embeddable Temporal worker for the Stigmer AI agent platform — handles agent execution, workflow orchestration, and MCP server management",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -104,10 +104,10 @@
|
|
|
104
104
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
105
105
|
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
|
106
106
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
107
|
-
"@stigmer/outbound": "3.
|
|
108
|
-
"@stigmer/protos": "3.
|
|
109
|
-
"@stigmer/temporal-codecs": "3.
|
|
110
|
-
"@stigmer/zip-structure": "3.
|
|
107
|
+
"@stigmer/outbound": "3.20.0",
|
|
108
|
+
"@stigmer/protos": "3.20.0",
|
|
109
|
+
"@stigmer/temporal-codecs": "3.20.0",
|
|
110
|
+
"@stigmer/zip-structure": "3.20.0",
|
|
111
111
|
"@temporalio/activity": "1.16.2",
|
|
112
112
|
"@temporalio/common": "1.16.2",
|
|
113
113
|
"@temporalio/interceptors-opentelemetry": "1.16.2",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the `stigmer_timing` lines a test subject writes to `console.log`
|
|
3
|
+
* while it runs, parsed, so a test can assert on a timeline the way the
|
|
4
|
+
* cluster's log reader will (`shared/cold-start-timing.ts`: the stdout line
|
|
5
|
+
* is the only place a timeline's segments exist).
|
|
6
|
+
*
|
|
7
|
+
* One helper for every suite that asserts on an emitted timeline (the
|
|
8
|
+
* runtime's `run-turn.test.ts`, each adapter's hermetic `turn-phases` proof),
|
|
9
|
+
* so the way a line is recognised — the selector as the FIRST key, the way
|
|
10
|
+
* `emitTimingLog` writes it — is decided once. Other log lines pass through
|
|
11
|
+
* to the real console untouched; nothing here silences the run.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** One parsed timeline line: the selector, `total_ms`, `segments`, and whatever context the emitter added. */
|
|
15
|
+
export interface TimingLine {
|
|
16
|
+
readonly stigmer_timing: string;
|
|
17
|
+
readonly total_ms: number;
|
|
18
|
+
readonly segments: ReadonlyArray<{ readonly name: string; readonly start_ms: number; readonly duration_ms: number }>;
|
|
19
|
+
readonly [key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run `act` with `console.log` observed; return every `stigmer_timing` line
|
|
24
|
+
* whose selector is `event`, in the order they were written.
|
|
25
|
+
*/
|
|
26
|
+
export async function captureTimingLines(event: string, act: () => Promise<void>): Promise<TimingLine[]> {
|
|
27
|
+
const prefix = `{"stigmer_timing":${JSON.stringify(event)}`;
|
|
28
|
+
const lines: TimingLine[] = [];
|
|
29
|
+
const original = console.log;
|
|
30
|
+
console.log = (...args: unknown[]): void => {
|
|
31
|
+
const first = args[0];
|
|
32
|
+
if (typeof first === "string" && first.startsWith(prefix)) {
|
|
33
|
+
lines.push(JSON.parse(first) as TimingLine);
|
|
34
|
+
}
|
|
35
|
+
original(...args);
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
await act();
|
|
39
|
+
} finally {
|
|
40
|
+
console.log = original;
|
|
41
|
+
}
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic proof: the Cursor harness gets the runtime's `turn_phases` timing
|
|
3
|
+
* line without a line of its own.
|
|
4
|
+
*
|
|
5
|
+
* Invariant pinned: driving the scripted Cursor SDK through the real Cursor
|
|
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 minted segment (two here: the text before the tool call and the
|
|
9
|
+
* text after it; the SDK's one `run_id` per `send()` is cut at every tool
|
|
10
|
+
* call, `translator.ts`), one tool span for the `read` call closed at the
|
|
11
|
+
* instant the SDK reported, the three root instants present, the adapter's
|
|
12
|
+
* own name as `harness`. The fold and its rules are
|
|
13
|
+
* `harness/__tests__/turn-timeline.test.ts`'s; the runtime's act of writing
|
|
14
|
+
* the line is `harness/__tests__/run-turn.test.ts`'s against the fake. What
|
|
15
|
+
* only this file can prove is that the Cursor translator emits the events the
|
|
16
|
+
* fold needs, in the order it needs them, from the SDK's own stream shape.
|
|
17
|
+
*
|
|
18
|
+
* This line is the runtime's; the adapter's own `turn_first_event` line
|
|
19
|
+
* (`turn-settle.ts`) measures the SDK's `send()` window, which the runtime
|
|
20
|
+
* cannot see, and is not asserted here.
|
|
21
|
+
*
|
|
22
|
+
* Structural assertions only: the scripted clock makes every number
|
|
23
|
+
* deterministic, but pinning one would pin the clock's tick quantum, not a
|
|
24
|
+
* behaviour. The status goldens beside this file are untouched by design —
|
|
25
|
+
* the line is stdout, never the status — and their byte-identity under this
|
|
26
|
+
* change is proved by running them.
|
|
27
|
+
*
|
|
28
|
+
* Same scenario as `tool-call.test.ts`, so the two files read together.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
32
|
+
|
|
33
|
+
vi.mock("@cursor/sdk", async () =>
|
|
34
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
35
|
+
);
|
|
36
|
+
vi.mock("@cursor/sdk/sqlite", async () =>
|
|
37
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSqliteModule(),
|
|
38
|
+
);
|
|
39
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
40
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
ScriptedClock,
|
|
45
|
+
createHermeticEnvironment,
|
|
46
|
+
type HermeticEnvironment,
|
|
47
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
48
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
49
|
+
import { captureTimingLines } from "../../../../__test-utils__/timing-lines.js";
|
|
50
|
+
import { TURN_PHASES_EVENT } from "../../../../harness/turn-timeline.js";
|
|
51
|
+
import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
|
|
52
|
+
import {
|
|
53
|
+
FIXTURE,
|
|
54
|
+
SDK_CATALOG,
|
|
55
|
+
beginCursorScenario,
|
|
56
|
+
cursorExecutionRecord,
|
|
57
|
+
runCursorTurn,
|
|
58
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
59
|
+
|
|
60
|
+
const AGENT_ID = "agent-hermetic-tool-0001";
|
|
61
|
+
const RUN_ID = "run-hermetic-tool-0001";
|
|
62
|
+
const CALL_ID = "call-hermetic-read-0001";
|
|
63
|
+
const USER_MESSAGE = "What is in README.md?";
|
|
64
|
+
const READ_ARGS = { path: "README.md" };
|
|
65
|
+
const READ_RESULT = "# Hermetic\n\nA fixture readme.\n";
|
|
66
|
+
const ASSISTANT_TEXT = "README.md holds a one-line fixture description.";
|
|
67
|
+
|
|
68
|
+
describe("ExecuteCursor hermetic — the runtime's turn_phases line", () => {
|
|
69
|
+
let env: HermeticEnvironment;
|
|
70
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
71
|
+
const clock = new ScriptedClock();
|
|
72
|
+
|
|
73
|
+
beforeAll(() => {
|
|
74
|
+
env = createHermeticEnvironment();
|
|
75
|
+
registry = stubRegistryFetch();
|
|
76
|
+
clock.install();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterAll(() => {
|
|
80
|
+
clock.uninstall();
|
|
81
|
+
registry.restore();
|
|
82
|
+
env.dispose();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("writes one line for the turn, reading the translator's segments and tool spans", async () => {
|
|
86
|
+
const ev = sdkEvents(AGENT_ID, RUN_ID);
|
|
87
|
+
const agent = new ScriptedCursorAgent({
|
|
88
|
+
agentId: AGENT_ID,
|
|
89
|
+
runIds: [RUN_ID],
|
|
90
|
+
observeStep: () => clock.tick(),
|
|
91
|
+
turns: [
|
|
92
|
+
[
|
|
93
|
+
step.event(ev.init()),
|
|
94
|
+
step.event(ev.assistant("Let me read it.")),
|
|
95
|
+
step.event(ev.toolCall(CALL_ID, "read", "running", READ_ARGS)),
|
|
96
|
+
step.event(ev.toolCall(CALL_ID, "read", "completed", READ_ARGS, READ_RESULT)),
|
|
97
|
+
step.event(ev.assistant(ASSISTANT_TEXT)),
|
|
98
|
+
step.turnEnded({ inputTokens: 2_000, outputTokens: 90, cacheReadTokens: 0, cacheWriteTokens: 0 }),
|
|
99
|
+
step.finished({ result: ASSISTANT_TEXT, model: { id: FIXTURE.model, params: [] } }),
|
|
100
|
+
],
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
104
|
+
const scenario = beginCursorScenario({
|
|
105
|
+
env,
|
|
106
|
+
clock,
|
|
107
|
+
record,
|
|
108
|
+
sdk: { agents: [agent], catalog: SDK_CATALOG },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const lines = await captureTimingLines(TURN_PHASES_EVENT, async () => {
|
|
112
|
+
const invocation = await runCursorTurn(scenario);
|
|
113
|
+
expect(invocation.outcome.kind).toBe("returned");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(lines, "one line per turn").toHaveLength(1);
|
|
117
|
+
const line = lines[0]!;
|
|
118
|
+
expect(line.harness, "the adapter's diagnostic name").toBe("cursor");
|
|
119
|
+
expect(line.outcome).toBe("completed");
|
|
120
|
+
expect(line.execution_id).toBe(record.execution.metadata!.id);
|
|
121
|
+
expect(line.turn_seq).toBeTypeOf("number");
|
|
122
|
+
|
|
123
|
+
expect(line.rounds, "the segment before the tool call and the one after it").toBe(2);
|
|
124
|
+
expect(line.tool_calls).toBe(1);
|
|
125
|
+
expect(line.sub_agents).toBe(0);
|
|
126
|
+
expect(line.segments.filter((s) => s.name === "model_round")).toHaveLength(2);
|
|
127
|
+
const toolSpan = line.segments.filter((s) => s.name === "tool:read");
|
|
128
|
+
expect(toolSpan).toHaveLength(1);
|
|
129
|
+
expect(toolSpan[0]!.duration_ms, "a span, not a point: the SDK's running and completed instants differ").toBeGreaterThan(0);
|
|
130
|
+
|
|
131
|
+
expect(line.first_event_ms).toBeTypeOf("number");
|
|
132
|
+
expect(line.first_visible_token_ms).toBeTypeOf("number");
|
|
133
|
+
expect(line.first_text_ms).toBeTypeOf("number");
|
|
134
|
+
expect(line.total_ms).toBeGreaterThanOrEqual(line.first_text_ms as number);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -122,7 +122,10 @@ export async function streamAndSettle(frame: CursorTurnFrame): Promise<TurnOutco
|
|
|
122
122
|
// `send_returned` covers the send() call itself, `first_delta` the wait
|
|
123
123
|
// until the SDK's first delta. Primary send only — the recovery retries
|
|
124
124
|
// below rebuild the agent and would skew the user-perceived turn start
|
|
125
|
-
// this measures. No delta (immediate pause/failure) → no line.
|
|
125
|
+
// this measures. No delta (immediate pause/failure) → no line. This line is
|
|
126
|
+
// the SDK's send window and nothing else: the cross-harness instants (first
|
|
127
|
+
// visible token, rounds, tool spans) are the runtime's `turn_phases` line
|
|
128
|
+
// (`harness/turn-timeline.ts`), written for every harness alike.
|
|
126
129
|
const turnStartTiming = new TimingRecorder();
|
|
127
130
|
let turnFirstEventEmitted = false;
|
|
128
131
|
const primaryOnDelta = makeCursorTurnOnDelta(onDeltaDeps);
|