@yaag/runtime 0.1.4 → 0.2.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/package.json +1 -1
- package/src/agent.ts +11 -3
- package/src/ask-contract-identity.ts +23 -5
- package/src/ask-exchange-events.ts +14 -1
- package/src/ask-exchange-options.ts +7 -2
- package/src/ask-exchange.ts +198 -26
- package/src/ask-hash.ts +7 -3
- package/src/ask-limit.ts +27 -11
- package/src/ask-output-steering.ts +1 -3
- package/src/ask-output.ts +20 -4
- package/src/ask-turn.ts +11 -0
- package/src/cassette-loader.ts +24 -0
- package/src/cassette-replay.ts +76 -7
- package/src/cassette-schema.ts +17 -2
- package/src/cassette.ts +10 -9
- package/src/checkpoint-flush.ts +101 -0
- package/src/connection.ts +20 -0
- package/src/define-agent.ts +11 -5
- package/src/errors.ts +11 -2
- package/src/events.ts +29 -3
- package/src/fake-transport.ts +55 -0
- package/src/frame-queue.ts +5 -0
- package/src/index.ts +11 -1
- package/src/model-resolution.ts +119 -0
- package/src/model-suffix.ts +24 -0
- package/src/pi-state.ts +63 -8
- package/src/recording-transport.ts +8 -8
- package/src/replay-divergence.ts +1 -0
- package/src/replay-transport.ts +4 -0
- package/src/report-result-extension.ts +128 -0
- package/src/report-result-output.ts +73 -0
- package/src/report-result-steering.ts +83 -0
- package/src/report-result.ts +122 -0
- package/src/resume-transport.ts +26 -8
- package/src/run-checkpoint.ts +93 -41
- package/src/run.ts +86 -32
- package/src/spawn.ts +29 -4
- package/src/stall-watchdog.ts +193 -0
- package/src/summary-agent.ts +11 -2
- package/src/summary.ts +23 -2
- package/src/thinking-level.ts +32 -0
- package/src/transport.ts +43 -8
- package/src/types.ts +50 -14
- package/src/validation-errors.ts +10 -4
package/src/errors.ts
CHANGED
|
@@ -7,12 +7,18 @@ export interface AskLimitOutcome {
|
|
|
7
7
|
readonly count: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
import type { AgentProgress } from "./pi-state.ts";
|
|
11
|
+
|
|
12
|
+
export type { AgentProgress } from "./pi-state.ts";
|
|
13
|
+
|
|
10
14
|
/** The recorded result when yaag rejects an Ask because no frame arrived within `idleMs`. */
|
|
11
15
|
export interface AskStalledOutcome {
|
|
12
16
|
/** The configured silence threshold that tripped. */
|
|
13
17
|
readonly idleMs: number;
|
|
14
|
-
/** True when
|
|
18
|
+
/** True when escalation failed to settle the Agent and yaag killed the process. */
|
|
15
19
|
readonly destructive: boolean;
|
|
20
|
+
/** State the Stall Watchdog probe observed; absent when the probe stayed silent. */
|
|
21
|
+
readonly state?: AgentProgress;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/** Recoverable result when all structured-output correction efforts are exhausted. */
|
|
@@ -30,7 +36,7 @@ export type YaagErrorCode =
|
|
|
30
36
|
| "AGENT_BUSY" // concurrent ask() on one Handle
|
|
31
37
|
| "ASK_TIMEOUT" // timeoutMs elapsed
|
|
32
38
|
| "ASK_LIMIT" // soft Ask budget exceeded after grace
|
|
33
|
-
| "ASK_STALLED" // no frame arrived within
|
|
39
|
+
| "ASK_STALLED" // no frame arrived within the silence budget; escalated, then kill
|
|
34
40
|
| "ASK_INVALID_OUTPUT" // settled text could not be extracted or satisfy outputSchema
|
|
35
41
|
| "ARGS_INVALID" // arguments failed schema validation before the Run started
|
|
36
42
|
| "OPTIONS_CONFLICT" // incompatible Run options were supplied
|
|
@@ -54,6 +60,8 @@ export class YaagError extends Error {
|
|
|
54
60
|
readonly idleMs?: number;
|
|
55
61
|
/** True when an `ASK_STALLED` escalation had to kill the Agent. */
|
|
56
62
|
readonly destructive?: boolean;
|
|
63
|
+
/** State observed by the Stall Watchdog probe, present only for `ASK_STALLED`. */
|
|
64
|
+
readonly state?: AgentProgress;
|
|
57
65
|
/** Number of corrective output steers sent, present only for `ASK_INVALID_OUTPUT`. */
|
|
58
66
|
readonly steeringEfforts?: number;
|
|
59
67
|
/** Localized extraction or schema errors, present only for `ASK_INVALID_OUTPUT`. */
|
|
@@ -76,6 +84,7 @@ export class YaagError extends Error {
|
|
|
76
84
|
if (options !== undefined && "idleMs" in options) {
|
|
77
85
|
this.idleMs = options.idleMs;
|
|
78
86
|
this.destructive = options.destructive;
|
|
87
|
+
if (options.state !== undefined) this.state = options.state;
|
|
79
88
|
}
|
|
80
89
|
if (options !== undefined && "steeringEfforts" in options) {
|
|
81
90
|
this.steeringEfforts = options.steeringEfforts;
|
package/src/events.ts
CHANGED
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
* activity, output, and usage observations. v0 renders them as stderr lines; the
|
|
7
7
|
* dedicated fd arrives with the extension.
|
|
8
8
|
*/
|
|
9
|
+
import type { SettlementCause } from "./stall-watchdog.ts";
|
|
9
10
|
import type { TokenBreakdown, WorktreeResolution } from "./transport.ts";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
export type { SettlementCause } from "./stall-watchdog.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A Run's outcome (ADR-0022). `paused` arrives with the pause slice.
|
|
16
|
+
* `interrupted` is not terminal: only an in-flight Checkpoint carries it, and a
|
|
17
|
+
* settled Run never writes it (ADR-0031).
|
|
18
|
+
*/
|
|
19
|
+
export type RunOutcome = "completed" | "failed" | "stopped" | "paused" | "interrupted";
|
|
13
20
|
|
|
14
21
|
/** The current, Ask-scoped observer projection derived from Agent frames. */
|
|
15
22
|
export type AgentActivity =
|
|
@@ -32,7 +39,16 @@ export interface NodeUsage {
|
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
export type LifecycleEventBody =
|
|
35
|
-
| {
|
|
42
|
+
| {
|
|
43
|
+
readonly type: "run_start";
|
|
44
|
+
readonly program: string;
|
|
45
|
+
/**
|
|
46
|
+
* Path of the in-progress Checkpoint this Run publishes at each Ask
|
|
47
|
+
* boundary (ADR-0031). A hard death leaves this file loadable with the
|
|
48
|
+
* outcome `interrupted`, so an observer can name the resume source.
|
|
49
|
+
*/
|
|
50
|
+
readonly artifact?: string;
|
|
51
|
+
}
|
|
36
52
|
| {
|
|
37
53
|
readonly type: "agent_spawn";
|
|
38
54
|
readonly agent: string;
|
|
@@ -96,6 +112,11 @@ export type LifecycleEventBody =
|
|
|
96
112
|
readonly ok: boolean;
|
|
97
113
|
/** Largest inter-frame silence during this Ask; absent during Cassette playback. */
|
|
98
114
|
readonly maxFrameGapMs?: number;
|
|
115
|
+
/**
|
|
116
|
+
* How the Ask settled. Absent means `normal`. `recovered` means the Stall
|
|
117
|
+
* Watchdog settled it from observed state, so its usage is an undercount.
|
|
118
|
+
*/
|
|
119
|
+
readonly cause?: SettlementCause;
|
|
99
120
|
}
|
|
100
121
|
| {
|
|
101
122
|
readonly type: "agent_usage";
|
|
@@ -128,6 +149,11 @@ export type LifecycleEventBody =
|
|
|
128
149
|
readonly incomplete: boolean;
|
|
129
150
|
/** Largest inter-frame silence across every Ask; 0 when nothing was measured. */
|
|
130
151
|
readonly worstFrameGapMs: number;
|
|
152
|
+
/**
|
|
153
|
+
* The Run failed, `--record` asked for a Checkpoint, and publication also
|
|
154
|
+
* failed. Carries the publication error text. Absent on every other path.
|
|
155
|
+
*/
|
|
156
|
+
readonly checkpointLost?: string;
|
|
131
157
|
};
|
|
132
158
|
|
|
133
159
|
/**
|
package/src/fake-transport.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_WRAP_UP_PROMPT } from "./ask-limit.ts";
|
|
2
2
|
import { FrameQueue } from "./frame-queue.ts";
|
|
3
3
|
import { parseFrame } from "./jsonl.ts";
|
|
4
|
+
import { isReportResultCommandFrame, REPORT_RESULT_TOOL_NAME } from "./report-result.ts";
|
|
4
5
|
import type { AgentStats, AgentTransport, AskMarker, Frame } from "./transport.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -37,10 +38,16 @@ export interface FakePromptScript {
|
|
|
37
38
|
export interface FakeTransportOptions extends FakePromptScript {
|
|
38
39
|
/** One script per Ask's initial prompt, used in order; preserves the single-script shorthand above. */
|
|
39
40
|
readonly scripts?: readonly FakePromptScript[];
|
|
41
|
+
/** Payload of a `get_state` probe; omission answers with an idle, non-streaming Agent. */
|
|
42
|
+
readonly state?: Record<string, unknown>;
|
|
43
|
+
/** Makes the fake ignore every `get_state` probe, as a wedged Agent does. */
|
|
44
|
+
readonly stateSilent?: boolean;
|
|
40
45
|
/** Makes a steer RPC response fail without embedding a limit decision in playback. */
|
|
41
46
|
readonly steerError?: string;
|
|
42
47
|
/** Makes an abort RPC response fail without embedding a limit decision in playback. */
|
|
43
48
|
readonly abortError?: string;
|
|
49
|
+
/** Makes the private report_result schema command fail. */
|
|
50
|
+
readonly schemaCommandError?: string;
|
|
44
51
|
readonly stats?: AgentStats;
|
|
45
52
|
/** pi can answer a command after `agent_settled` — it is last among events only. */
|
|
46
53
|
readonly promptResponse?: "immediate" | "after-settle";
|
|
@@ -86,6 +93,7 @@ export class FakeTransport implements AgentTransport {
|
|
|
86
93
|
if (frame.type === "steer") void this.#answerSteer(frame);
|
|
87
94
|
if (frame.type === "abort") void this.#answerAbort(frame);
|
|
88
95
|
if (frame.type === "get_last_assistant_text") this.#answerLastText(frame);
|
|
96
|
+
if (frame.type === "get_state") this.#answerState(frame);
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
frames(): AsyncIterable<Frame> {
|
|
@@ -127,12 +135,23 @@ export class FakeTransport implements AgentTransport {
|
|
|
127
135
|
return this.#closing;
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
/** Pushes one unsolicited frame, as a late or abandoned turn would emit. */
|
|
139
|
+
emit(frame: Frame): void {
|
|
140
|
+
this.#queue.push(frame);
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
/** Simulates the process dying: the frame stream just ends. */
|
|
131
144
|
die(): void {
|
|
132
145
|
this.#queue.end();
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
async #answerPrompt(frame: Frame): Promise<void> {
|
|
149
|
+
// pi answers an extension command without starting a turn, so the private
|
|
150
|
+
// schema command selects no script and plays no frames (ADR-0032).
|
|
151
|
+
if (isReportResultCommandFrame(frame)) {
|
|
152
|
+
this.#answerControl(frame, this.#options.schemaCommandError);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
136
155
|
const script = this.#selectPromptScript();
|
|
137
156
|
const error = script.promptError ?? this.#options.promptError;
|
|
138
157
|
const response = {
|
|
@@ -214,6 +233,23 @@ export class FakeTransport implements AgentTransport {
|
|
|
214
233
|
}
|
|
215
234
|
}
|
|
216
235
|
|
|
236
|
+
#answerState(frame: Frame): void {
|
|
237
|
+
if (this.#options.stateSilent === true) return;
|
|
238
|
+
this.#queue.push({
|
|
239
|
+
type: "response",
|
|
240
|
+
command: "get_state",
|
|
241
|
+
id: frame.id,
|
|
242
|
+
success: true,
|
|
243
|
+
data: this.#options.state ?? {
|
|
244
|
+
model: { provider: "test", id: "model" },
|
|
245
|
+
isStreaming: false,
|
|
246
|
+
isCompacting: false,
|
|
247
|
+
messageCount: 1,
|
|
248
|
+
pendingMessageCount: 0,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
217
253
|
#answerLastText(frame: Frame): void {
|
|
218
254
|
const text = this.#currentScript?.lastText ?? this.#options.lastText;
|
|
219
255
|
this.#queue.push({
|
|
@@ -226,6 +262,25 @@ export class FakeTransport implements AgentTransport {
|
|
|
226
262
|
}
|
|
227
263
|
}
|
|
228
264
|
|
|
265
|
+
/** The frames one accepted `report_result` call produces, for scripted Asks. */
|
|
266
|
+
export function reportResultFrames(value: unknown, toolCallId = "call-1"): readonly Frame[] {
|
|
267
|
+
return [
|
|
268
|
+
{
|
|
269
|
+
type: "tool_execution_start",
|
|
270
|
+
toolCallId,
|
|
271
|
+
toolName: REPORT_RESULT_TOOL_NAME,
|
|
272
|
+
args: value,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
type: "tool_execution_end",
|
|
276
|
+
toolCallId,
|
|
277
|
+
toolName: REPORT_RESULT_TOOL_NAME,
|
|
278
|
+
result: { content: [], details: { reportedResult: value }, terminate: true },
|
|
279
|
+
isError: false,
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
|
|
229
284
|
/**
|
|
230
285
|
* Loads and parses a named JSONL frame fixture from the runtime fixtures directory.
|
|
231
286
|
*
|
package/src/frame-queue.ts
CHANGED
|
@@ -23,6 +23,11 @@ export class FrameQueue {
|
|
|
23
23
|
this.#buffered.push(frame);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Drops every buffered frame the consumer has not taken yet. */
|
|
27
|
+
discardPending(): void {
|
|
28
|
+
this.#buffered = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
/** Ends the stream; the consumer's iteration finishes once drained. */
|
|
27
32
|
end(): void {
|
|
28
33
|
if (this.#ended) return;
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type {
|
|
|
9
9
|
CassetteSpawn,
|
|
10
10
|
} from "./cassette.ts";
|
|
11
11
|
export { CASSETTE_VERSION } from "./cassette.ts";
|
|
12
|
-
export { loadCassette } from "./cassette-loader.ts";
|
|
12
|
+
export { assertReplayable, loadCassette } from "./cassette-loader.ts";
|
|
13
13
|
export type { AgentConfig, AgentDefinition } from "./define-agent.ts";
|
|
14
14
|
export { agentDefinitionConfig, defineAgent, isAgentDefinition } from "./define-agent.ts";
|
|
15
15
|
export type { OrchestrationProgram, ProgramDefinition } from "./define-run.ts";
|
|
@@ -32,6 +32,15 @@ export type {
|
|
|
32
32
|
NodeUsage,
|
|
33
33
|
StampedEventSink,
|
|
34
34
|
} from "./events.ts";
|
|
35
|
+
export type {
|
|
36
|
+
ModelError,
|
|
37
|
+
ModelErrorReason,
|
|
38
|
+
ModelResolver,
|
|
39
|
+
ModelSelection,
|
|
40
|
+
ModelSpec,
|
|
41
|
+
ThinkingResolver,
|
|
42
|
+
ThinkingSpec,
|
|
43
|
+
} from "./model-resolution.ts";
|
|
35
44
|
export type { DecodedNode, NodeDecoder } from "./node-decoder.ts";
|
|
36
45
|
export { DEFAULT_NODE_DECODERS } from "./node-decoders.ts";
|
|
37
46
|
export type { NodePath } from "./node-path.ts";
|
|
@@ -79,6 +88,7 @@ export type {
|
|
|
79
88
|
export type {
|
|
80
89
|
AskOptions,
|
|
81
90
|
Handle,
|
|
91
|
+
ResolvedSpawnOptions,
|
|
82
92
|
SpawnOptions,
|
|
83
93
|
SpawnOverrides,
|
|
84
94
|
StructuredAskOptions,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { parseModelSuffix } from "./model-suffix.ts";
|
|
2
|
+
import { isThinkingLevel, type ThinkingLevel } from "./thinking-level.ts";
|
|
3
|
+
|
|
4
|
+
/** Why one model candidate was rejected by pi. */
|
|
5
|
+
export type ModelErrorReason = "not_found" | "auth" | "rate_limited";
|
|
6
|
+
|
|
7
|
+
/** One failed Model Resolution attempt, handed back to the caller's resolver. */
|
|
8
|
+
export interface ModelError {
|
|
9
|
+
readonly reason: ModelErrorReason;
|
|
10
|
+
/** The model pattern that failed, after inline-suffix stripping. */
|
|
11
|
+
readonly failedModel: string;
|
|
12
|
+
/** 0-based index of the attempt that produced this error. */
|
|
13
|
+
readonly attempt: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Picks the next model candidate, or `undefined` to give up. */
|
|
17
|
+
export type ModelResolver = (errors: readonly ModelError[]) => string | undefined;
|
|
18
|
+
|
|
19
|
+
/** Every accepted `model` form: one pattern, an ordered list, or a resolver. */
|
|
20
|
+
export type ModelSpec = string | readonly string[] | ModelResolver;
|
|
21
|
+
|
|
22
|
+
/** Picks the thinking level for a settled model, or `undefined` for pi's default. */
|
|
23
|
+
export type ThinkingResolver = (
|
|
24
|
+
selectedModel: string,
|
|
25
|
+
errors: readonly ModelError[],
|
|
26
|
+
) => ThinkingLevel | undefined;
|
|
27
|
+
|
|
28
|
+
/** Every accepted `thinking` form: one level or a resolver. */
|
|
29
|
+
export type ThinkingSpec = ThinkingLevel | ThinkingResolver;
|
|
30
|
+
|
|
31
|
+
/** One attempt's settled selection; `model: undefined` inherits pi's default model. */
|
|
32
|
+
export interface ModelSelection {
|
|
33
|
+
readonly model: string | undefined;
|
|
34
|
+
readonly thinking: ThinkingLevel | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The canonical shape the spawn loop consumes, whatever form the caller wrote. */
|
|
38
|
+
export interface ModelResolution {
|
|
39
|
+
/** The next selection given the errors so far, or `undefined` when candidates run out. */
|
|
40
|
+
resolve(errors: readonly ModelError[]): ModelSelection | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Turns any accepted `model`/`thinking` form into one canonical resolver pair.
|
|
45
|
+
*
|
|
46
|
+
* Within one attempt the order is: model resolver → inline-suffix parse →
|
|
47
|
+
* thinking resolver, and an inline suffix wins over the `thinking` spec, which
|
|
48
|
+
* is then not consulted at all. An absent `model` inherits pi's default for
|
|
49
|
+
* attempt 0 only; a thinking *resolver* is then not called, because no model has
|
|
50
|
+
* settled, while a thinking level still applies.
|
|
51
|
+
* Array specs are copied, so later caller mutation cannot change resolution.
|
|
52
|
+
* Throws a `TypeError` when a caller resolver returns something other than a
|
|
53
|
+
* non-empty model string or a valid thinking level.
|
|
54
|
+
*/
|
|
55
|
+
export function normalizeModelResolution(spec: {
|
|
56
|
+
readonly model?: ModelSpec;
|
|
57
|
+
readonly thinking?: ThinkingSpec;
|
|
58
|
+
}): ModelResolution {
|
|
59
|
+
const model = toModelResolver(spec.model);
|
|
60
|
+
const thinking = toThinkingResolver(spec.thinking);
|
|
61
|
+
const literalThinking = typeof spec.thinking === "function" ? undefined : spec.thinking;
|
|
62
|
+
return {
|
|
63
|
+
resolve(errors: readonly ModelError[]): ModelSelection | undefined {
|
|
64
|
+
if (model === undefined) {
|
|
65
|
+
if (errors.length > 0) return undefined;
|
|
66
|
+
return { model: undefined, thinking: checkedThinking(literalThinking) };
|
|
67
|
+
}
|
|
68
|
+
const pattern = checkedModel(model(errors));
|
|
69
|
+
if (pattern === undefined) return undefined;
|
|
70
|
+
const parsed = parseModelSuffix(pattern);
|
|
71
|
+
if (parsed.thinking !== undefined) return parsed;
|
|
72
|
+
return { model: parsed.model, thinking: checkedThinking(thinking(parsed.model, errors)) };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toModelResolver(spec: ModelSpec | undefined): ModelResolver | undefined {
|
|
78
|
+
if (spec === undefined) return undefined;
|
|
79
|
+
if (typeof spec === "string") {
|
|
80
|
+
return (errors) => (errors.length === 0 ? spec : undefined);
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(spec)) {
|
|
83
|
+
const candidates: readonly string[] = [...spec];
|
|
84
|
+
return (errors) => candidates[errors.length];
|
|
85
|
+
}
|
|
86
|
+
if (typeof spec === "function") return spec;
|
|
87
|
+
throw new TypeError(`model must be a string, an array of strings, or a function: ${show(spec)}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toThinkingResolver(spec: ThinkingSpec | undefined): ThinkingResolver {
|
|
91
|
+
if (spec === undefined) return () => undefined;
|
|
92
|
+
if (typeof spec === "function") return spec;
|
|
93
|
+
if (isThinkingLevel(spec)) return () => spec;
|
|
94
|
+
throw new TypeError(`thinking must be a pi thinking level or a function: ${show(spec)}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function checkedModel(value: unknown): string | undefined {
|
|
98
|
+
if (value === undefined) return undefined;
|
|
99
|
+
if (typeof value !== "string" || value === "") {
|
|
100
|
+
throw new TypeError(
|
|
101
|
+
`model resolver must return a non-empty string or undefined: ${show(value)}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkedThinking(value: unknown): ThinkingLevel | undefined {
|
|
108
|
+
if (value === undefined) return undefined;
|
|
109
|
+
if (!isThinkingLevel(value)) {
|
|
110
|
+
throw new TypeError(
|
|
111
|
+
`thinking resolver must return a pi thinking level or undefined: ${show(value)}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function show(value: unknown): string {
|
|
118
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
119
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isThinkingLevel, type ThinkingLevel } from "./thinking-level.ts";
|
|
2
|
+
|
|
3
|
+
/** A model pattern split into its model part and its optional inline thinking suffix. */
|
|
4
|
+
export interface ModelPattern {
|
|
5
|
+
readonly model: string;
|
|
6
|
+
readonly thinking: ThinkingLevel | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Splits a resolved model pattern on its inline thinking suffix.
|
|
11
|
+
*
|
|
12
|
+
* The split happens on the *last* `:` only when the tail is exactly one of pi's
|
|
13
|
+
* thinking levels and the head is non-empty, so colon-bearing model ids such as
|
|
14
|
+
* `llama3:8b` and suffix-only strings such as `:medium` survive untouched. The
|
|
15
|
+
* parser is total: it never throws, and an unusable pattern simply fails later
|
|
16
|
+
* as an ordinary model lookup failure.
|
|
17
|
+
*/
|
|
18
|
+
export function parseModelSuffix(pattern: string): ModelPattern {
|
|
19
|
+
const separator = pattern.lastIndexOf(":");
|
|
20
|
+
if (separator <= 0) return { model: pattern, thinking: undefined };
|
|
21
|
+
const tail = pattern.slice(separator + 1);
|
|
22
|
+
if (!isThinkingLevel(tail)) return { model: pattern, thinking: undefined };
|
|
23
|
+
return { model: pattern.slice(0, separator), thinking: tail };
|
|
24
|
+
}
|
package/src/pi-state.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { REPORT_RESULT_EXTENSION_PATH, REPORT_RESULT_TOOL_NAME } from "./report-result.ts";
|
|
1
2
|
import type { AgentStats, Frame, OpenOptions, TokenBreakdown } from "./transport.ts";
|
|
2
3
|
|
|
4
|
+
/** What the Agent reported about its own work, read from a `get_state` probe. */
|
|
5
|
+
export interface AgentProgress {
|
|
6
|
+
/** True while pi streams a completion, compacts its context, or holds a queued prompt. */
|
|
7
|
+
readonly working: boolean;
|
|
8
|
+
readonly streaming: boolean;
|
|
9
|
+
readonly compacting: boolean;
|
|
10
|
+
/** Prompts pi has queued but not started. */
|
|
11
|
+
readonly pendingMessages: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
/** The command line for one Agent. */
|
|
4
15
|
export function piCommand(options: OpenOptions, toolProbeExtensionPath?: string): string[] {
|
|
5
16
|
const cmd = ["pi", "--mode", "rpc"];
|
|
@@ -22,13 +33,7 @@ function appendCapabilities(
|
|
|
22
33
|
toolProbeExtensionPath: string | undefined,
|
|
23
34
|
): void {
|
|
24
35
|
const hermetic = options.inherit !== true;
|
|
25
|
-
|
|
26
|
-
if (hermetic) cmd.push("--no-tools");
|
|
27
|
-
} else if (options.tools.length === 0) {
|
|
28
|
-
cmd.push("--no-tools");
|
|
29
|
-
} else {
|
|
30
|
-
cmd.push("--tools", options.tools.join(","));
|
|
31
|
-
}
|
|
36
|
+
appendTools(cmd, options, hermetic);
|
|
32
37
|
if (options.resolvedSkillPaths === undefined) {
|
|
33
38
|
if (hermetic) cmd.push("--no-skills");
|
|
34
39
|
} else {
|
|
@@ -39,6 +44,10 @@ function appendCapabilities(
|
|
|
39
44
|
cmd.push("--no-extensions", "--no-context-files", "--no-prompt-templates");
|
|
40
45
|
}
|
|
41
46
|
for (const path of options.resolvedExtensionPaths ?? []) cmd.push("-e", path);
|
|
47
|
+
// Every Agent carries the report-result tool, because an Ask declares its
|
|
48
|
+
// output schema long after the process started and pi loads extensions only
|
|
49
|
+
// at startup (ADR-0032).
|
|
50
|
+
cmd.push("-e", REPORT_RESULT_EXTENSION_PATH);
|
|
42
51
|
if (
|
|
43
52
|
options.tools !== undefined &&
|
|
44
53
|
options.tools.length > 0 &&
|
|
@@ -47,10 +56,34 @@ function appendCapabilities(
|
|
|
47
56
|
cmd.push("-e", toolProbeExtensionPath);
|
|
48
57
|
}
|
|
49
58
|
if (options.disallowedTools !== undefined) {
|
|
50
|
-
|
|
59
|
+
// The private tool is never a program's to deny: excluding it would fail
|
|
60
|
+
// every schema-bearing Ask with "agent did not call report_result". A
|
|
61
|
+
// denylist of only that name leaves nothing to exclude.
|
|
62
|
+
const denied = options.disallowedTools.filter((name) => name !== REPORT_RESULT_TOOL_NAME);
|
|
63
|
+
// An explicit empty denylist still emits its empty flag, as it always has.
|
|
64
|
+
const emitsExcludeTools = denied.length > 0 || options.disallowedTools.length === 0;
|
|
65
|
+
if (emitsExcludeTools) cmd.push("--exclude-tools", denied.join(","));
|
|
51
66
|
}
|
|
52
67
|
}
|
|
53
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Expresses the tool baseline as an allowlist that keeps `report_result`.
|
|
71
|
+
*
|
|
72
|
+
* `--no-tools` strips extension tools as well, and refuses a tool registered
|
|
73
|
+
* afterwards, so "no tools" becomes an allowlist of exactly yaag's own tool
|
|
74
|
+
* (e2e/report-result-spike.test.ts). The tool stays inactive until a
|
|
75
|
+
* schema-bearing Ask activates it, so the Agent's effective capability is
|
|
76
|
+
* unchanged.
|
|
77
|
+
*/
|
|
78
|
+
function appendTools(cmd: string[], options: OpenOptions, hermetic: boolean): void {
|
|
79
|
+
if (options.tools === undefined) {
|
|
80
|
+
if (hermetic) cmd.push("--tools", REPORT_RESULT_TOOL_NAME);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const allowed = [...new Set([...options.tools, REPORT_RESULT_TOOL_NAME])];
|
|
84
|
+
cmd.push("--tools", allowed.join(","));
|
|
85
|
+
}
|
|
86
|
+
|
|
54
87
|
/**
|
|
55
88
|
* `provider/id` from a `get_state` response — what the Agent actually resolved
|
|
56
89
|
* to, not the pattern that was requested. Null when the payload has no model.
|
|
@@ -66,6 +99,28 @@ export function readModel(response: Frame): string | null {
|
|
|
66
99
|
return provider ? `${provider}/${model.id}` : model.id;
|
|
67
100
|
}
|
|
68
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Reads what a `get_state` payload says about the Agent's own work.
|
|
104
|
+
*
|
|
105
|
+
* Null when the payload does not carry pi's streaming flags, which the Stall
|
|
106
|
+
* Watchdog treats as work in progress rather than as a settlement.
|
|
107
|
+
*/
|
|
108
|
+
export function readAgentProgress(data: unknown): AgentProgress | null {
|
|
109
|
+
if (typeof data !== "object" || data === null) return null;
|
|
110
|
+
const record: Record<string, unknown> = { ...data };
|
|
111
|
+
if (typeof record.isStreaming !== "boolean") return null;
|
|
112
|
+
const streaming = record.isStreaming;
|
|
113
|
+
const compacting = record.isCompacting === true;
|
|
114
|
+
const pendingMessages =
|
|
115
|
+
typeof record.pendingMessageCount === "number" ? record.pendingMessageCount : 0;
|
|
116
|
+
return {
|
|
117
|
+
working: streaming || compacting || pendingMessages > 0,
|
|
118
|
+
streaming,
|
|
119
|
+
compacting,
|
|
120
|
+
pendingMessages,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
69
124
|
/** Returns pi's persisted session path from a startup `get_state` response, when valid. */
|
|
70
125
|
export function readSessionFile(response: Frame): string | null {
|
|
71
126
|
const data: unknown = response.data;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CassetteSink } from "./cassette.ts";
|
|
2
|
-
import type { AskLimitOutcome, AskStalledOutcome } from "./errors.ts";
|
|
3
2
|
import { readGitFacts } from "./git-facts.ts";
|
|
4
3
|
import type {
|
|
5
4
|
AgentStats,
|
|
6
5
|
AgentTransport,
|
|
6
|
+
AskCompletion,
|
|
7
7
|
AskMarker,
|
|
8
8
|
AskPlayback,
|
|
9
9
|
Frame,
|
|
@@ -75,13 +75,13 @@ class RecordingTransport implements AgentTransport {
|
|
|
75
75
|
return this.#inner.beginAsk(marker);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
finishAsk(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.#inner.
|
|
78
|
+
finishAsk(completion: AskCompletion): void {
|
|
79
|
+
this.#recorder.finishAsk(completion);
|
|
80
|
+
this.#inner.finishAsk(completion);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
recordedExtractionPolicy(index: number): string | undefined {
|
|
84
|
+
return this.#inner.recordedExtractionPolicy?.(index);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
close(): Promise<AgentStats> {
|
package/src/replay-divergence.ts
CHANGED
package/src/replay-transport.ts
CHANGED
|
@@ -65,6 +65,10 @@ class ReplayTransport implements AgentTransport {
|
|
|
65
65
|
// A completed replay does not alter the recorded Cassette.
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
recordedExtractionPolicy(index: number): string | undefined {
|
|
69
|
+
return this.#replay.agent.asks[index]?.extractionPolicy;
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
close(): Promise<AgentStats> {
|
|
69
73
|
this.#closed ??= Promise.resolve(this.#replay.stats).finally(() => this.#replay.finish());
|
|
70
74
|
return this.#closed;
|