@yaag/runtime 0.1.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/package.json +25 -0
- package/src/agent-names.ts +20 -0
- package/src/agent-usage.ts +72 -0
- package/src/agent.ts +130 -0
- package/src/args-validation.ts +11 -0
- package/src/ask-activity.ts +84 -0
- package/src/ask-contract-identity.ts +96 -0
- package/src/ask-exchange-events.ts +60 -0
- package/src/ask-exchange-options.ts +32 -0
- package/src/ask-exchange.ts +291 -0
- package/src/ask-hash.ts +86 -0
- package/src/ask-limit.ts +189 -0
- package/src/ask-output-steering.ts +69 -0
- package/src/ask-output-tail.ts +166 -0
- package/src/ask-output.ts +109 -0
- package/src/ask-settlement.ts +37 -0
- package/src/ask-turn.ts +70 -0
- package/src/cassette-loader.ts +131 -0
- package/src/cassette-publish.ts +55 -0
- package/src/cassette-replay.ts +178 -0
- package/src/cassette-schema.ts +152 -0
- package/src/cassette.ts +275 -0
- package/src/checkpoint-dir.ts +89 -0
- package/src/connection.ts +123 -0
- package/src/define-agent.ts +83 -0
- package/src/define-run.ts +69 -0
- package/src/errors.ts +115 -0
- package/src/events.ts +143 -0
- package/src/extension-package.ts +88 -0
- package/src/extension-paths.ts +66 -0
- package/src/extension-source.ts +60 -0
- package/src/fake-transport.ts +240 -0
- package/src/frame-gap.ts +41 -0
- package/src/frame-queue.ts +52 -0
- package/src/git-facts.ts +32 -0
- package/src/idle-watch.ts +154 -0
- package/src/index.ts +96 -0
- package/src/jsonl.ts +42 -0
- package/src/live-transport.ts +210 -0
- package/src/node-decoder-subagent.ts +67 -0
- package/src/node-decoder-workflow.ts +74 -0
- package/src/node-decoder.ts +23 -0
- package/src/node-decoders.ts +9 -0
- package/src/node-details.ts +70 -0
- package/src/node-path.ts +36 -0
- package/src/node-tracker.ts +143 -0
- package/src/pi-state.ts +108 -0
- package/src/prompt-gist.ts +13 -0
- package/src/prompt.ts +80 -0
- package/src/reap.ts +59 -0
- package/src/recording-transport.ts +97 -0
- package/src/replay-divergence.ts +155 -0
- package/src/replay-transport.ts +72 -0
- package/src/resume-preconditions.ts +59 -0
- package/src/resume-transport.ts +165 -0
- package/src/run-checkpoint.ts +93 -0
- package/src/run-context.ts +19 -0
- package/src/run.ts +274 -0
- package/src/skill-probe.ts +247 -0
- package/src/skill-restriction-transport.ts +76 -0
- package/src/spawn.ts +241 -0
- package/src/summary-agent.ts +310 -0
- package/src/summary-nodes.ts +77 -0
- package/src/summary.ts +213 -0
- package/src/tool-probe-extension.ts +17 -0
- package/src/tool-probe.ts +141 -0
- package/src/transport.ts +178 -0
- package/src/types.ts +130 -0
- package/src/validation-errors.ts +70 -0
- package/src/wire-constants.ts +24 -0
- package/src/worktree-transport.ts +125 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { AgentActivity } from "./events.ts";
|
|
2
|
+
import type { NodeInfo } from "./summary-nodes.ts";
|
|
3
|
+
import type { TokenBreakdown, WorktreeResolution } from "./transport.ts";
|
|
4
|
+
|
|
5
|
+
export type { AgentActivity } from "./events.ts";
|
|
6
|
+
export type { NodeInfo } from "./summary-nodes.ts";
|
|
7
|
+
|
|
8
|
+
/** The observer-facing lifecycle state of an Agent. */
|
|
9
|
+
export type AgentState = "idle" | "asking" | "exited";
|
|
10
|
+
|
|
11
|
+
/** Identity and accounting facts that apply in every observer Agent state. */
|
|
12
|
+
interface AgentInfoBase {
|
|
13
|
+
readonly model: string | null;
|
|
14
|
+
readonly cwd: string | null;
|
|
15
|
+
readonly branch: string | null;
|
|
16
|
+
/** pi's session file for this Agent, when the spawn reported one; a Peek reads it. */
|
|
17
|
+
readonly sessionFile: string | null;
|
|
18
|
+
readonly activity: AgentActivity | null;
|
|
19
|
+
readonly tokens: TokenBreakdown | null;
|
|
20
|
+
readonly cost: number | null;
|
|
21
|
+
readonly incomplete: boolean;
|
|
22
|
+
readonly stateChangedAt: number | null;
|
|
23
|
+
/** Timestamp ordering only cumulative live accounting, never lifecycle state. */
|
|
24
|
+
readonly usageUpdatedAt: number | null;
|
|
25
|
+
readonly askStartedAt: number | null;
|
|
26
|
+
/** This Agent's bounded Nested Node table, in first-seen order (spec §3). */
|
|
27
|
+
readonly nodes: readonly NodeInfo[];
|
|
28
|
+
/** Exited Nested Nodes dropped to keep the table bounded. */
|
|
29
|
+
readonly finishedNodesPruned: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** An Agent between Asks; its latest settled Ask identity, if any, is retained. */
|
|
33
|
+
export interface IdleAgentInfo extends AgentInfoBase {
|
|
34
|
+
readonly state: "idle";
|
|
35
|
+
readonly askIndex: number | null;
|
|
36
|
+
readonly promptGist: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** An Agent with one active Ask, whose identity and start timestamp are known. */
|
|
40
|
+
export interface AskingAgentInfo extends AgentInfoBase {
|
|
41
|
+
readonly state: "asking";
|
|
42
|
+
readonly askIndex: number;
|
|
43
|
+
readonly promptGist: string;
|
|
44
|
+
/** Whether the active Ask is served from Cassette playback. */
|
|
45
|
+
readonly replayed: boolean;
|
|
46
|
+
/** Null only for a legacy unstamped Ask start; no timestamp is synthesized. */
|
|
47
|
+
readonly askStartedAt: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A terminal Agent observation retaining its final usage and latest Ask identity. */
|
|
51
|
+
export interface ExitedAgentInfo extends AgentInfoBase {
|
|
52
|
+
readonly state: "exited";
|
|
53
|
+
readonly askIndex: number | null;
|
|
54
|
+
readonly promptGist: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The discriminated observer projection for one Agent.
|
|
59
|
+
*
|
|
60
|
+
* Transition helpers below are pure and clock-free. Stamped state events older
|
|
61
|
+
* than the folded state are ignored; unstamped legacy events use stream order.
|
|
62
|
+
*/
|
|
63
|
+
export type AgentInfo = IdleAgentInfo | AskingAgentInfo | ExitedAgentInfo;
|
|
64
|
+
|
|
65
|
+
/** Internal fold record, deliberately identical to the public observer projection. */
|
|
66
|
+
export type AgentRecord = AgentInfo;
|
|
67
|
+
|
|
68
|
+
/** Cumulative assistant-completion usage observed while an Agent is still live. */
|
|
69
|
+
export interface AgentUsageObservation {
|
|
70
|
+
readonly tokens: TokenBreakdown;
|
|
71
|
+
readonly cost: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Final usage and optional worktree identity reported when an Agent exits. */
|
|
75
|
+
export interface AgentExitObservation {
|
|
76
|
+
readonly tokens: TokenBreakdown | null;
|
|
77
|
+
readonly cost: number | null;
|
|
78
|
+
readonly incomplete: boolean;
|
|
79
|
+
readonly worktree?: WorktreeResolution;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Produces an idle placeholder for lifecycle events received before a spawn. */
|
|
83
|
+
function placeholderAgent(): IdleAgentInfo {
|
|
84
|
+
return {
|
|
85
|
+
model: null,
|
|
86
|
+
cwd: null,
|
|
87
|
+
branch: null,
|
|
88
|
+
sessionFile: null,
|
|
89
|
+
state: "idle",
|
|
90
|
+
askIndex: null,
|
|
91
|
+
promptGist: null,
|
|
92
|
+
activity: null,
|
|
93
|
+
tokens: null,
|
|
94
|
+
cost: null,
|
|
95
|
+
incomplete: false,
|
|
96
|
+
stateChangedAt: null,
|
|
97
|
+
usageUpdatedAt: null,
|
|
98
|
+
askStartedAt: null,
|
|
99
|
+
nodes: [],
|
|
100
|
+
finishedNodesPruned: 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reconciles spawn identity without reopening a terminal Agent or losing facts
|
|
106
|
+
* learned from a prior exit. A late spawn fills only missing identity fields.
|
|
107
|
+
*/
|
|
108
|
+
export function spawnAgent(
|
|
109
|
+
current: AgentRecord | undefined,
|
|
110
|
+
identity: {
|
|
111
|
+
readonly model: string;
|
|
112
|
+
readonly cwd: string;
|
|
113
|
+
readonly branch?: string;
|
|
114
|
+
readonly sessionFile?: string;
|
|
115
|
+
},
|
|
116
|
+
at: number | null,
|
|
117
|
+
): AgentRecord {
|
|
118
|
+
if (current === undefined) {
|
|
119
|
+
return {
|
|
120
|
+
...placeholderAgent(),
|
|
121
|
+
model: identity.model,
|
|
122
|
+
cwd: identity.cwd,
|
|
123
|
+
branch: identity.branch ?? null,
|
|
124
|
+
sessionFile: identity.sessionFile ?? null,
|
|
125
|
+
stateChangedAt: at,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
...current,
|
|
130
|
+
model: current.model ?? identity.model,
|
|
131
|
+
cwd: current.cwd ?? identity.cwd,
|
|
132
|
+
branch: current.branch ?? identity.branch ?? null,
|
|
133
|
+
sessionFile: current.sessionFile ?? identity.sessionFile ?? null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Folds an Ask start into the asking arm when it is not older than the folded
|
|
139
|
+
* Agent state or Ask identity. Legacy unstamped events retain arrival order.
|
|
140
|
+
*/
|
|
141
|
+
export function startAsk(
|
|
142
|
+
current: AgentRecord | undefined,
|
|
143
|
+
ask: { readonly index: number; readonly promptGist: string; readonly replayed: boolean },
|
|
144
|
+
at: number | null,
|
|
145
|
+
): AgentRecord {
|
|
146
|
+
const agent = current ?? placeholderAgent();
|
|
147
|
+
if (!canChangeAskState(agent, at) || (agent.askIndex !== null && ask.index < agent.askIndex)) {
|
|
148
|
+
return agent;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
...agent,
|
|
152
|
+
state: "asking",
|
|
153
|
+
askIndex: ask.index,
|
|
154
|
+
promptGist: ask.promptGist,
|
|
155
|
+
replayed: ask.replayed,
|
|
156
|
+
activity: null,
|
|
157
|
+
stateChangedAt: at,
|
|
158
|
+
askStartedAt: at,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Folds an Ask-scoped activity only into its current asking Agent. Older stamped
|
|
164
|
+
* observations, mismatched Ask identities, and terminal states are ignored.
|
|
165
|
+
*/
|
|
166
|
+
export function setActivity(
|
|
167
|
+
current: AgentRecord | undefined,
|
|
168
|
+
index: number,
|
|
169
|
+
activity: AgentActivity,
|
|
170
|
+
at: number | null,
|
|
171
|
+
): AgentRecord {
|
|
172
|
+
const agent = current ?? placeholderAgent();
|
|
173
|
+
if (
|
|
174
|
+
agent.state !== "asking" ||
|
|
175
|
+
agent.askIndex !== index ||
|
|
176
|
+
(at !== null && agent.stateChangedAt !== null && at < agent.stateChangedAt)
|
|
177
|
+
) {
|
|
178
|
+
return agent;
|
|
179
|
+
}
|
|
180
|
+
return { ...agent, activity, stateChangedAt: at };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Folds an Ask settlement into the idle arm, retaining the settled Ask identity.
|
|
185
|
+
* Older stamped events are ignored and legacy unstamped events use stream order.
|
|
186
|
+
*/
|
|
187
|
+
export function endAsk(
|
|
188
|
+
current: AgentRecord | undefined,
|
|
189
|
+
index: number,
|
|
190
|
+
at: number | null,
|
|
191
|
+
): AgentRecord {
|
|
192
|
+
const agent = current ?? placeholderAgent();
|
|
193
|
+
if (
|
|
194
|
+
!canChangeAskState(agent, at) ||
|
|
195
|
+
(agent.askIndex !== null &&
|
|
196
|
+
(index < agent.askIndex || (agent.state === "asking" && agent.askIndex !== index)))
|
|
197
|
+
) {
|
|
198
|
+
return agent;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
...agent,
|
|
202
|
+
state: "idle",
|
|
203
|
+
askIndex: index,
|
|
204
|
+
activity: null,
|
|
205
|
+
stateChangedAt: at,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Folds a cumulative usage snapshot without changing Agent lifecycle state.
|
|
211
|
+
*
|
|
212
|
+
* Terminal Agent accounting is never replaced; equal usage timestamps retain
|
|
213
|
+
* stream order while older stamped observations are ignored.
|
|
214
|
+
*/
|
|
215
|
+
export function setUsage(
|
|
216
|
+
current: AgentRecord | undefined,
|
|
217
|
+
observation: AgentUsageObservation,
|
|
218
|
+
at: number | null,
|
|
219
|
+
): AgentRecord {
|
|
220
|
+
const agent = current ?? placeholderAgent();
|
|
221
|
+
if (
|
|
222
|
+
agent.state === "exited" ||
|
|
223
|
+
(at !== null && agent.usageUpdatedAt !== null && at < agent.usageUpdatedAt)
|
|
224
|
+
) {
|
|
225
|
+
return agent;
|
|
226
|
+
}
|
|
227
|
+
return { ...agent, tokens: observation.tokens, cost: observation.cost, usageUpdatedAt: at };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Folds authoritative shutdown accounting into the terminal Agent state.
|
|
232
|
+
*
|
|
233
|
+
* Worktree identity is enriched even when a stale exit cannot alter state or
|
|
234
|
+
* usage, so late observations remain useful.
|
|
235
|
+
*/
|
|
236
|
+
export function exitAgent(
|
|
237
|
+
current: AgentRecord | undefined,
|
|
238
|
+
observation: AgentExitObservation,
|
|
239
|
+
at: number | null,
|
|
240
|
+
): AgentRecord {
|
|
241
|
+
const agent = enrichWorktree(current ?? placeholderAgent(), observation.worktree);
|
|
242
|
+
if (!canReplaceExit(agent, at)) return agent;
|
|
243
|
+
return {
|
|
244
|
+
...agent,
|
|
245
|
+
state: "exited",
|
|
246
|
+
activity: null,
|
|
247
|
+
tokens: observation.tokens,
|
|
248
|
+
cost: observation.cost,
|
|
249
|
+
incomplete: observation.incomplete || observation.cost === null,
|
|
250
|
+
stateChangedAt: at,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Computes Run-wide accounting from every Agent's newest snapshot. A live Agent
|
|
256
|
+
* without an assistant completion contributes zero; an exited Agent without
|
|
257
|
+
* final stats preserves the established unknown-accounting semantics.
|
|
258
|
+
*/
|
|
259
|
+
export function totalsFromAgents(agents: Readonly<Record<string, AgentRecord>>): {
|
|
260
|
+
readonly cost: number;
|
|
261
|
+
readonly tokens: TokenBreakdown | null;
|
|
262
|
+
readonly incomplete: boolean;
|
|
263
|
+
} {
|
|
264
|
+
const records = Object.values(agents);
|
|
265
|
+
return {
|
|
266
|
+
cost: records.reduce((total, agent) => total + (agent.cost ?? 0), 0),
|
|
267
|
+
tokens: records.some((agent) => agent.state === "exited" && agent.tokens === null)
|
|
268
|
+
? null
|
|
269
|
+
: records.reduce(addTokens, zeroTokens()),
|
|
270
|
+
incomplete: records.some(
|
|
271
|
+
(agent) => agent.state === "exited" && (agent.incomplete || agent.cost === null),
|
|
272
|
+
),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function enrichWorktree(agent: AgentRecord, worktree: WorktreeResolution | undefined): AgentRecord {
|
|
277
|
+
if (worktree === undefined) return agent;
|
|
278
|
+
return {
|
|
279
|
+
...agent,
|
|
280
|
+
cwd: agent.cwd ?? worktree.cwd,
|
|
281
|
+
branch: agent.branch ?? worktree.branch,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function canChangeAskState(agent: AgentRecord, at: number | null): boolean {
|
|
286
|
+
return (
|
|
287
|
+
agent.state !== "exited" &&
|
|
288
|
+
(at === null || agent.stateChangedAt === null || at >= agent.stateChangedAt)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function canReplaceExit(agent: AgentRecord, at: number | null): boolean {
|
|
293
|
+
return at === null || agent.stateChangedAt === null || at >= agent.stateChangedAt;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function addTokens(total: TokenBreakdown, agent: AgentRecord): TokenBreakdown {
|
|
297
|
+
const tokens = agent.tokens;
|
|
298
|
+
if (tokens === null) return total;
|
|
299
|
+
return {
|
|
300
|
+
input: total.input + tokens.input,
|
|
301
|
+
output: total.output + tokens.output,
|
|
302
|
+
cacheRead: total.cacheRead + tokens.cacheRead,
|
|
303
|
+
cacheWrite: total.cacheWrite + tokens.cacheWrite,
|
|
304
|
+
total: total.total + tokens.total,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function zeroTokens(): TokenBreakdown {
|
|
309
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
310
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { LifecycleEventBody, NodeState } from "./events.ts";
|
|
2
|
+
import type { AgentRecord } from "./summary-agent.ts";
|
|
3
|
+
import type { TokenBreakdown } from "./transport.ts";
|
|
4
|
+
import { AGENT_NODE_TABLE_MAX } from "./wire-constants.ts";
|
|
5
|
+
|
|
6
|
+
/** One Nested Node's folded row in an Agent's bounded node table. */
|
|
7
|
+
export interface NodeInfo {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly state: NodeState;
|
|
10
|
+
readonly activityGist: string | null;
|
|
11
|
+
readonly tokens: TokenBreakdown | null;
|
|
12
|
+
readonly cost: number | null;
|
|
13
|
+
readonly updatedAt: number | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type NodeUpdateEvent = Extract<LifecycleEventBody, { readonly type: "node_update" }>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Folds one `node_update` into an Agent's bounded node table.
|
|
20
|
+
*
|
|
21
|
+
* Rows upsert by path and keep first-seen order. A stamped update older than
|
|
22
|
+
* the folded row is ignored, mirroring `setActivity`. Past
|
|
23
|
+
* `AGENT_NODE_TABLE_MAX` rows, exited nodes prune oldest-first into
|
|
24
|
+
* `finishedNodesPruned`. Running nodes are never pruned, so the table retains
|
|
25
|
+
* at most `max(AGENT_NODE_TABLE_MAX, simultaneously running node count)` rows:
|
|
26
|
+
* the row count is unbounded if upstream concurrency is unbounded. Only each
|
|
27
|
+
* row's snapshot size is bounded (spec §3).
|
|
28
|
+
*/
|
|
29
|
+
export function applyNodeUpdate(
|
|
30
|
+
agent: AgentRecord,
|
|
31
|
+
event: NodeUpdateEvent,
|
|
32
|
+
at: number | null,
|
|
33
|
+
): AgentRecord {
|
|
34
|
+
const index = agent.nodes.findIndex((node) => node.path === event.path);
|
|
35
|
+
const current = index === -1 ? undefined : agent.nodes[index];
|
|
36
|
+
if (
|
|
37
|
+
current !== undefined &&
|
|
38
|
+
at !== null &&
|
|
39
|
+
current.updatedAt !== null &&
|
|
40
|
+
at < current.updatedAt
|
|
41
|
+
) {
|
|
42
|
+
return agent;
|
|
43
|
+
}
|
|
44
|
+
const node = nodeFrom(event, at);
|
|
45
|
+
const nodes =
|
|
46
|
+
current === undefined
|
|
47
|
+
? [...agent.nodes, node]
|
|
48
|
+
: agent.nodes.map((existing, position) => (position === index ? node : existing));
|
|
49
|
+
return prune({ ...agent, nodes });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function nodeFrom(event: NodeUpdateEvent, at: number | null): NodeInfo {
|
|
53
|
+
return {
|
|
54
|
+
path: event.path,
|
|
55
|
+
state: event.state,
|
|
56
|
+
activityGist: event.activityGist ?? null,
|
|
57
|
+
tokens: event.usage?.tokens ?? null,
|
|
58
|
+
cost: event.usage?.cost ?? null,
|
|
59
|
+
updatedAt: at,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function prune(agent: AgentRecord): AgentRecord {
|
|
64
|
+
if (agent.nodes.length <= AGENT_NODE_TABLE_MAX) return agent;
|
|
65
|
+
const excess = agent.nodes.length - AGENT_NODE_TABLE_MAX;
|
|
66
|
+
const kept: NodeInfo[] = [];
|
|
67
|
+
let pruned = 0;
|
|
68
|
+
for (const node of agent.nodes) {
|
|
69
|
+
if (node.state !== "running" && pruned < excess) {
|
|
70
|
+
pruned += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
kept.push(node);
|
|
74
|
+
}
|
|
75
|
+
if (pruned === 0) return agent;
|
|
76
|
+
return { ...agent, nodes: kept, finishedNodesPruned: agent.finishedNodesPruned + pruned };
|
|
77
|
+
}
|
package/src/summary.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { LifecycleEvent, LifecycleEventBody, RunOutcome } from "./events.ts";
|
|
2
|
+
import type { AgentInfo } from "./summary-agent.ts";
|
|
3
|
+
import {
|
|
4
|
+
type AgentRecord,
|
|
5
|
+
endAsk,
|
|
6
|
+
exitAgent,
|
|
7
|
+
setActivity,
|
|
8
|
+
setUsage,
|
|
9
|
+
spawnAgent,
|
|
10
|
+
startAsk,
|
|
11
|
+
totalsFromAgents,
|
|
12
|
+
} from "./summary-agent.ts";
|
|
13
|
+
import { applyNodeUpdate } from "./summary-nodes.ts";
|
|
14
|
+
import type { TokenBreakdown } from "./transport.ts";
|
|
15
|
+
|
|
16
|
+
export type { NodeState, NodeUsage, RunOutcome } from "./events.ts";
|
|
17
|
+
export type {
|
|
18
|
+
AgentActivity,
|
|
19
|
+
AgentInfo,
|
|
20
|
+
AgentState,
|
|
21
|
+
AskingAgentInfo,
|
|
22
|
+
ExitedAgentInfo,
|
|
23
|
+
IdleAgentInfo,
|
|
24
|
+
NodeInfo,
|
|
25
|
+
} from "./summary-agent.ts";
|
|
26
|
+
|
|
27
|
+
/** The observer-facing lifecycle state of a Run. */
|
|
28
|
+
export type RunState = "running" | "ended";
|
|
29
|
+
|
|
30
|
+
/** Accounting and identity facts shared by all Run observer states. */
|
|
31
|
+
interface RunSummaryBase {
|
|
32
|
+
readonly program: string;
|
|
33
|
+
readonly startedAt: number | null;
|
|
34
|
+
readonly agents: Readonly<Record<string, AgentInfo>>;
|
|
35
|
+
readonly asksStarted: number;
|
|
36
|
+
readonly asksSettled: number;
|
|
37
|
+
readonly cost: number;
|
|
38
|
+
readonly tokens: TokenBreakdown | null;
|
|
39
|
+
readonly incomplete: boolean;
|
|
40
|
+
readonly durationMs: number;
|
|
41
|
+
readonly worstFrameGapMs: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A Run that has not settled; it has neither outcome nor compatibility result. */
|
|
45
|
+
export interface RunningRunSummary extends RunSummaryBase {
|
|
46
|
+
readonly runState: "running";
|
|
47
|
+
readonly outcome: null;
|
|
48
|
+
readonly ok: null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A settled Run with its outcome, compatibility result, and ending event time. */
|
|
52
|
+
export interface EndedRunSummary extends RunSummaryBase {
|
|
53
|
+
readonly runState: "ended";
|
|
54
|
+
readonly outcome: RunOutcome;
|
|
55
|
+
readonly ok: boolean;
|
|
56
|
+
/** The `run_end.at` event fact used to reject strictly older stamped endings. */
|
|
57
|
+
readonly endedAt: number | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The pure, discriminated observer fold of a Run's Lifecycle Events.
|
|
62
|
+
*
|
|
63
|
+
* It stores event facts only, reads no clock, and never synthesizes durations:
|
|
64
|
+
* `run_end.durationMs` is authoritative. Unknown events are no-ops. Stamped
|
|
65
|
+
* state events use timestamp order (ties retain stream order); legacy unstamped
|
|
66
|
+
* bodies use arrival order because no timestamp can be compared.
|
|
67
|
+
*/
|
|
68
|
+
export type RunSummary = RunningRunSummary | EndedRunSummary;
|
|
69
|
+
|
|
70
|
+
const ZERO_TOKENS: TokenBreakdown = {
|
|
71
|
+
input: 0,
|
|
72
|
+
output: 0,
|
|
73
|
+
cacheRead: 0,
|
|
74
|
+
cacheWrite: 0,
|
|
75
|
+
total: 0,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Returns the deterministic, running Summary for a Run that emitted nothing. */
|
|
79
|
+
export function initialSummary(): RunningRunSummary {
|
|
80
|
+
return {
|
|
81
|
+
program: "",
|
|
82
|
+
runState: "running",
|
|
83
|
+
outcome: null,
|
|
84
|
+
startedAt: null,
|
|
85
|
+
agents: {},
|
|
86
|
+
asksStarted: 0,
|
|
87
|
+
asksSettled: 0,
|
|
88
|
+
cost: 0,
|
|
89
|
+
tokens: ZERO_TOKENS,
|
|
90
|
+
incomplete: false,
|
|
91
|
+
durationMs: 0,
|
|
92
|
+
worstFrameGapMs: 0,
|
|
93
|
+
ok: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Folds one Lifecycle Event into a fresh Summary without mutating either input.
|
|
99
|
+
*
|
|
100
|
+
* A stamped `run_end` older than the folded terminal timestamp is a no-op;
|
|
101
|
+
* equal times retain stream order. Legacy unstamped endings retain arrival order
|
|
102
|
+
* and may replace a stamped ending because no ordering fact exists. A late
|
|
103
|
+
* `run_start` enriches program/start metadata but never reopens a settled Run.
|
|
104
|
+
*/
|
|
105
|
+
export function applyEvent(summary: RunSummary, event: LifecycleEvent): RunSummary;
|
|
106
|
+
export function applyEvent(summary: RunSummary, event: LifecycleEventBody): RunSummary;
|
|
107
|
+
export function applyEvent(
|
|
108
|
+
summary: RunSummary,
|
|
109
|
+
event: LifecycleEvent | LifecycleEventBody,
|
|
110
|
+
): RunSummary {
|
|
111
|
+
const at = "at" in event ? event.at : null;
|
|
112
|
+
switch (event.type) {
|
|
113
|
+
case "run_start":
|
|
114
|
+
return { ...summary, program: event.program, startedAt: at };
|
|
115
|
+
case "agent_spawn":
|
|
116
|
+
return withAgent(summary, event.agent, spawnAgent(summary.agents[event.agent], event, at));
|
|
117
|
+
case "ask_start":
|
|
118
|
+
return withAgent(
|
|
119
|
+
{ ...summary, asksStarted: summary.asksStarted + 1 },
|
|
120
|
+
event.agent,
|
|
121
|
+
startAsk(summary.agents[event.agent], { ...event, replayed: event.replayed === true }, at),
|
|
122
|
+
);
|
|
123
|
+
case "ask_activity":
|
|
124
|
+
return updateActivity(summary, event, at);
|
|
125
|
+
case "node_update":
|
|
126
|
+
return updateNodes(summary, event, at);
|
|
127
|
+
case "agent_usage":
|
|
128
|
+
return withAgent(summary, event.agent, setUsage(summary.agents[event.agent], event, at));
|
|
129
|
+
case "ask_end":
|
|
130
|
+
return settleAsk(summary, event, at);
|
|
131
|
+
case "agent_exit":
|
|
132
|
+
return withAgent(summary, event.agent, exitAgent(summary.agents[event.agent], event, at));
|
|
133
|
+
case "run_end":
|
|
134
|
+
return endRun(summary, event, at);
|
|
135
|
+
default:
|
|
136
|
+
return summary;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function endRun(
|
|
141
|
+
summary: RunSummary,
|
|
142
|
+
event: Extract<LifecycleEventBody, { readonly type: "run_end" }>,
|
|
143
|
+
at: number | null,
|
|
144
|
+
): RunSummary {
|
|
145
|
+
if (
|
|
146
|
+
summary.runState === "ended" &&
|
|
147
|
+
at !== null &&
|
|
148
|
+
summary.endedAt !== null &&
|
|
149
|
+
at < summary.endedAt
|
|
150
|
+
) {
|
|
151
|
+
return summary;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
...summary,
|
|
155
|
+
runState: "ended",
|
|
156
|
+
outcome: event.outcome,
|
|
157
|
+
ok: event.ok,
|
|
158
|
+
endedAt: at,
|
|
159
|
+
durationMs: event.durationMs,
|
|
160
|
+
worstFrameGapMs: event.worstFrameGapMs,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function updateActivity(
|
|
165
|
+
summary: RunSummary,
|
|
166
|
+
event: Extract<LifecycleEventBody, { readonly type: "ask_activity" }>,
|
|
167
|
+
at: number | null,
|
|
168
|
+
): RunSummary {
|
|
169
|
+
const current = summary.agents[event.agent];
|
|
170
|
+
if (current === undefined) return summary;
|
|
171
|
+
const agent = setActivity(current, event.index, event.activity, at);
|
|
172
|
+
return current === agent
|
|
173
|
+
? summary
|
|
174
|
+
: { ...summary, agents: { ...summary.agents, [event.agent]: agent } };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function updateNodes(
|
|
178
|
+
summary: RunSummary,
|
|
179
|
+
event: Extract<LifecycleEventBody, { readonly type: "node_update" }>,
|
|
180
|
+
at: number | null,
|
|
181
|
+
): RunSummary {
|
|
182
|
+
const current = summary.agents[event.agent];
|
|
183
|
+
if (current === undefined) return summary;
|
|
184
|
+
const agent = applyNodeUpdate(current, event, at);
|
|
185
|
+
return current === agent
|
|
186
|
+
? summary
|
|
187
|
+
: { ...summary, agents: { ...summary.agents, [event.agent]: agent } };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function settleAsk(
|
|
191
|
+
summary: RunSummary,
|
|
192
|
+
event: Extract<LifecycleEventBody, { readonly type: "ask_end" }>,
|
|
193
|
+
at: number | null,
|
|
194
|
+
): RunSummary {
|
|
195
|
+
const next = withAgent(
|
|
196
|
+
{ ...summary, asksSettled: summary.asksSettled + 1 },
|
|
197
|
+
event.agent,
|
|
198
|
+
endAsk(summary.agents[event.agent], event.index, at),
|
|
199
|
+
);
|
|
200
|
+
return {
|
|
201
|
+
...next,
|
|
202
|
+
worstFrameGapMs:
|
|
203
|
+
event.maxFrameGapMs === undefined
|
|
204
|
+
? next.worstFrameGapMs
|
|
205
|
+
: Math.max(next.worstFrameGapMs, event.maxFrameGapMs),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function withAgent(summary: RunSummary, name: string, agent: AgentRecord): RunSummary {
|
|
210
|
+
const agents = { ...summary.agents, [name]: agent };
|
|
211
|
+
const totals = totalsFromAgents(agents);
|
|
212
|
+
return { ...summary, agents, ...totals };
|
|
213
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface ToolProbeAPI {
|
|
2
|
+
getActiveTools(): string[];
|
|
3
|
+
on(event: "session_start", handler: (event: unknown, context: ToolProbeContext) => void): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface ToolProbeContext {
|
|
7
|
+
readonly ui: { setStatus(key: string, text: string | undefined): void };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const STATUS_KEY = "yaag.tool-probe.v1";
|
|
11
|
+
|
|
12
|
+
/** Reports pi's post-composition active tool names through the RPC UI channel. */
|
|
13
|
+
export default function (pi: ToolProbeAPI): void {
|
|
14
|
+
pi.on("session_start", (_event, ctx) => {
|
|
15
|
+
ctx.ui.setStatus(STATUS_KEY, JSON.stringify(pi.getActiveTools()));
|
|
16
|
+
});
|
|
17
|
+
}
|