@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
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yaag/runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"!src/**/*.test.ts",
|
|
10
|
+
"!src/fixtures"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test": "bun test src",
|
|
20
|
+
"test:e2e": "bun test e2e"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"typebox": "1.3.7"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every Lifecycle Event identifies its Agent, so an unnamed Agent still needs a
|
|
3
|
+
* name and a duplicate is suffixed rather than rejected — spawning in a loop
|
|
4
|
+
* must not fail.
|
|
5
|
+
*
|
|
6
|
+
* `taken` is mutated: the chosen name is reserved.
|
|
7
|
+
*/
|
|
8
|
+
export function uniqueAgentName(
|
|
9
|
+
requested: string | undefined,
|
|
10
|
+
ordinal: number,
|
|
11
|
+
taken: Set<string>,
|
|
12
|
+
): string {
|
|
13
|
+
const base = requested ?? `a${ordinal + 1}`;
|
|
14
|
+
let name = base;
|
|
15
|
+
for (let suffix = 2; taken.has(name); suffix++) {
|
|
16
|
+
name = `${base}-${suffix}`;
|
|
17
|
+
}
|
|
18
|
+
taken.add(name);
|
|
19
|
+
return name;
|
|
20
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Frame, TokenBreakdown } from "./transport.ts";
|
|
2
|
+
|
|
3
|
+
export interface AgentUsageSnapshot {
|
|
4
|
+
readonly tokens: TokenBreakdown;
|
|
5
|
+
readonly cost: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Accumulates complete assistant `message_end` usage across an Agent's lifetime.
|
|
10
|
+
*
|
|
11
|
+
* Invalid or non-assistant frames never change the reported accounting.
|
|
12
|
+
*/
|
|
13
|
+
export class AgentUsage {
|
|
14
|
+
readonly #report: (snapshot: AgentUsageSnapshot) => void;
|
|
15
|
+
#tokens: TokenBreakdown = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
16
|
+
#cost = 0;
|
|
17
|
+
|
|
18
|
+
constructor(report: (snapshot: AgentUsageSnapshot) => void) {
|
|
19
|
+
this.#report = report;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Observes one raw Agent frame and reports after each valid assistant completion. */
|
|
23
|
+
observe(frame: Frame): void {
|
|
24
|
+
const usage = usageFromFrame(frame);
|
|
25
|
+
if (usage === null) return;
|
|
26
|
+
this.#tokens = {
|
|
27
|
+
input: this.#tokens.input + usage.tokens.input,
|
|
28
|
+
output: this.#tokens.output + usage.tokens.output,
|
|
29
|
+
cacheRead: this.#tokens.cacheRead + usage.tokens.cacheRead,
|
|
30
|
+
cacheWrite: this.#tokens.cacheWrite + usage.tokens.cacheWrite,
|
|
31
|
+
total: this.#tokens.total + usage.tokens.total,
|
|
32
|
+
};
|
|
33
|
+
this.#cost += usage.cost;
|
|
34
|
+
this.#report({ tokens: { ...this.#tokens }, cost: this.#cost });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function usageFromFrame(frame: Frame): AgentUsageSnapshot | null {
|
|
39
|
+
if (frame.type !== "message_end" || !isRecord(frame.message)) return null;
|
|
40
|
+
if (frame.message.role !== "assistant" || !isRecord(frame.message.usage)) return null;
|
|
41
|
+
const usage = frame.message.usage;
|
|
42
|
+
if (!isNumber(usage.input) || !isNumber(usage.output) || !isNumber(usage.cacheRead)) return null;
|
|
43
|
+
if (!isNumber(usage.cacheWrite) || !isNumber(usage.totalTokens) || !isRecord(usage.cost))
|
|
44
|
+
return null;
|
|
45
|
+
if (
|
|
46
|
+
!isNumber(usage.cost.input) ||
|
|
47
|
+
!isNumber(usage.cost.output) ||
|
|
48
|
+
!isNumber(usage.cost.cacheRead) ||
|
|
49
|
+
!isNumber(usage.cost.cacheWrite) ||
|
|
50
|
+
!isNumber(usage.cost.total)
|
|
51
|
+
) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
tokens: {
|
|
56
|
+
input: usage.input,
|
|
57
|
+
output: usage.output,
|
|
58
|
+
cacheRead: usage.cacheRead,
|
|
59
|
+
cacheWrite: usage.cacheWrite,
|
|
60
|
+
total: usage.totalTokens,
|
|
61
|
+
},
|
|
62
|
+
cost: usage.cost.total,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
67
|
+
return typeof value === "object" && value !== null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isNumber(value: unknown): value is number {
|
|
71
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
72
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Static, TSchema } from "typebox";
|
|
2
|
+
import { AgentUsage } from "./agent-usage.ts";
|
|
3
|
+
import { exchangeAsk } from "./ask-exchange.ts";
|
|
4
|
+
import type { EffectiveAskOptions } from "./ask-hash.ts";
|
|
5
|
+
import { Connection } from "./connection.ts";
|
|
6
|
+
import { agentError } from "./errors.ts";
|
|
7
|
+
import type { EventSink } from "./events.ts";
|
|
8
|
+
import type { AgentStats, AgentTransport } from "./transport.ts";
|
|
9
|
+
import type { AskOptions, Handle, SpawnOptions, StructuredAskOptions } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
export interface AgentOptions {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly cwd: string;
|
|
14
|
+
readonly branch: string | undefined;
|
|
15
|
+
readonly transport: AgentTransport;
|
|
16
|
+
readonly emit: EventSink;
|
|
17
|
+
/** The options this Agent was spawned with, for the ADR-0014 Ask hash. */
|
|
18
|
+
readonly spawnOptions: SpawnOptions;
|
|
19
|
+
/** Definition-owned defaults merged below explicit per-Ask options. */
|
|
20
|
+
readonly askDefaults?: AskOptions;
|
|
21
|
+
/** Definition identity recorded on its Asks, outside replay identity. */
|
|
22
|
+
readonly definitionName?: string;
|
|
23
|
+
/** Test-only override for the fixed duration-limit grace. */
|
|
24
|
+
readonly askLimitGraceMs?: number;
|
|
25
|
+
/** Test-only override for the bounded wait for `agent_settled` after an idle abort. */
|
|
26
|
+
readonly idleAbortSettleMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One Agent as an Orchestration Program sees it. Lives above the transport seam. */
|
|
30
|
+
export class Agent implements Handle {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly cwd: string;
|
|
33
|
+
readonly branch: string | undefined;
|
|
34
|
+
readonly model: string;
|
|
35
|
+
|
|
36
|
+
readonly #transport: AgentTransport;
|
|
37
|
+
readonly #connection: Connection;
|
|
38
|
+
readonly #emit: EventSink;
|
|
39
|
+
readonly #spawnOptions: SpawnOptions;
|
|
40
|
+
readonly #askDefaults: AskOptions;
|
|
41
|
+
readonly #definitionName: string | undefined;
|
|
42
|
+
readonly #askLimitGraceMs: number | undefined;
|
|
43
|
+
readonly #idleAbortSettleMs: number | undefined;
|
|
44
|
+
readonly #usage: AgentUsage;
|
|
45
|
+
#busy = false;
|
|
46
|
+
#incomplete = false;
|
|
47
|
+
#askIndex = 0;
|
|
48
|
+
#closing: Promise<AgentStats> | null = null;
|
|
49
|
+
|
|
50
|
+
constructor(options: AgentOptions) {
|
|
51
|
+
this.name = options.name;
|
|
52
|
+
this.cwd = options.cwd;
|
|
53
|
+
this.branch = options.branch;
|
|
54
|
+
this.model = options.transport.model;
|
|
55
|
+
this.#transport = options.transport;
|
|
56
|
+
this.#emit = options.emit;
|
|
57
|
+
this.#spawnOptions = options.spawnOptions;
|
|
58
|
+
this.#askDefaults = options.askDefaults ?? {};
|
|
59
|
+
this.#definitionName = options.definitionName;
|
|
60
|
+
this.#askLimitGraceMs = options.askLimitGraceMs;
|
|
61
|
+
this.#idleAbortSettleMs = options.idleAbortSettleMs;
|
|
62
|
+
this.#usage = new AgentUsage((snapshot) =>
|
|
63
|
+
this.#emit({ type: "agent_usage", agent: this.name, ...snapshot }),
|
|
64
|
+
);
|
|
65
|
+
this.#connection = new Connection(options.transport, options.name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async ask<Schema extends TSchema>(
|
|
69
|
+
prompt: string,
|
|
70
|
+
options: StructuredAskOptions<Schema>,
|
|
71
|
+
): Promise<Static<Schema>>;
|
|
72
|
+
async ask(prompt: string, options?: AskOptions): Promise<string>;
|
|
73
|
+
async ask(prompt: string, options?: EffectiveAskOptions): Promise<unknown> {
|
|
74
|
+
if (this.#connection.dead)
|
|
75
|
+
throw agentError(this.name, "AGENT_DIED", "agent is no longer running");
|
|
76
|
+
if (this.#busy) {
|
|
77
|
+
throw agentError(
|
|
78
|
+
this.name,
|
|
79
|
+
"AGENT_BUSY",
|
|
80
|
+
"an Ask is already in flight — is an await missing?",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
this.#busy = true;
|
|
84
|
+
const index = this.#askIndex++;
|
|
85
|
+
try {
|
|
86
|
+
return await exchangeAsk({
|
|
87
|
+
agent: this.name,
|
|
88
|
+
connection: this.#connection,
|
|
89
|
+
transport: this.#transport,
|
|
90
|
+
prompt,
|
|
91
|
+
index,
|
|
92
|
+
ask: { ...this.#askDefaults, ...options },
|
|
93
|
+
spawnOptions: this.#spawnOptions,
|
|
94
|
+
definitionName: this.#definitionName,
|
|
95
|
+
emit: this.#emit,
|
|
96
|
+
usage: this.#usage,
|
|
97
|
+
close: () => void this.close().catch(() => {}),
|
|
98
|
+
askLimitGraceMs: this.#askLimitGraceMs,
|
|
99
|
+
idleAbortSettleMs: this.#idleAbortSettleMs,
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
this.#busy = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** True when the Agent was killed mid-Ask, so its cost is a floor (ADR-0012). */
|
|
107
|
+
get incomplete(): boolean {
|
|
108
|
+
return this.#incomplete;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Shuts the Agent down and reports its cost. Idempotent. */
|
|
112
|
+
close(): Promise<AgentStats> {
|
|
113
|
+
this.#closing ??= this.#shutdown(this.#busy);
|
|
114
|
+
return this.#closing;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async #shutdown(midAsk: boolean): Promise<AgentStats> {
|
|
118
|
+
this.#incomplete = midAsk;
|
|
119
|
+
const stats = await this.#transport.close();
|
|
120
|
+
this.#emit({
|
|
121
|
+
type: "agent_exit",
|
|
122
|
+
agent: this.name,
|
|
123
|
+
tokens: stats.tokens,
|
|
124
|
+
cost: stats.cost,
|
|
125
|
+
incomplete: midAsk,
|
|
126
|
+
...(this.branch === undefined ? {} : { worktree: { cwd: this.cwd, branch: this.branch } }),
|
|
127
|
+
});
|
|
128
|
+
return stats;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TSchema } from "typebox";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
import { YaagError } from "./errors.ts";
|
|
4
|
+
import { formatValidationErrors } from "./validation-errors.ts";
|
|
5
|
+
|
|
6
|
+
/** Throws ARGS_INVALID when value violates schema, without changing value. */
|
|
7
|
+
export function validateArgs(schema: TSchema, value: unknown): void {
|
|
8
|
+
const errors = Value.Errors(schema, value);
|
|
9
|
+
if (errors.length === 0) return;
|
|
10
|
+
throw new YaagError("ARGS_INVALID", formatValidationErrors(value, errors).join("\n"));
|
|
11
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AgentActivity } from "./events.ts";
|
|
2
|
+
import type { Frame } from "./transport.ts";
|
|
3
|
+
import { TOOL_ARGS_GIST_MAX_CHARS } from "./wire-constants.ts";
|
|
4
|
+
|
|
5
|
+
/** Reports complete Ask activity transitions derived from already-observed frames. */
|
|
6
|
+
export class AskActivityTracker {
|
|
7
|
+
readonly #onChange: (activity: AgentActivity) => void;
|
|
8
|
+
#last: AgentActivity | null = null;
|
|
9
|
+
|
|
10
|
+
constructor(onChange: (activity: AgentActivity) => void) {
|
|
11
|
+
this.#onChange = onChange;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Inspects one Agent frame and ignores malformed or unsupported shapes. */
|
|
15
|
+
observe(frame: Frame): void {
|
|
16
|
+
const activity = activityFromFrame(frame);
|
|
17
|
+
if (activity === null || sameActivity(activity, this.#last)) return;
|
|
18
|
+
this.#last = activity;
|
|
19
|
+
this.#onChange(activity);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function activityFromFrame(frame: Frame): AgentActivity | null {
|
|
24
|
+
if (frame.type === "message_update") return messageActivity(frame.assistantMessageEvent);
|
|
25
|
+
if (frame.type === "tool_execution_start") return toolActivity(frame);
|
|
26
|
+
if (frame.type === "compaction_start") return { type: "compacting" };
|
|
27
|
+
if (frame.type === "auto_retry_start" || frame.type === "summarization_retry_start") {
|
|
28
|
+
return retryActivity(frame);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function messageActivity(value: unknown): AgentActivity | null {
|
|
34
|
+
if (!isRecord(value) || typeof value.type !== "string") return null;
|
|
35
|
+
if (value.type === "thinking_start" || value.type === "thinking_delta")
|
|
36
|
+
return { type: "thinking" };
|
|
37
|
+
if (value.type === "text_start" || value.type === "text_delta") return { type: "writing" };
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toolActivity(frame: Frame): AgentActivity | null {
|
|
42
|
+
if (typeof frame.toolName !== "string") return null;
|
|
43
|
+
return { type: "tool", name: frame.toolName, argsGist: toolArgsGist(frame.args) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function retryActivity(frame: Frame): AgentActivity | null {
|
|
47
|
+
if (typeof frame.attempt !== "number" || typeof frame.maxAttempts !== "number") return null;
|
|
48
|
+
return { type: "retrying", attempt: frame.attempt, max: frame.maxAttempts };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toolArgsGist(args: unknown): string {
|
|
52
|
+
const command = isRecord(args) && typeof args.command === "string" ? args.command : undefined;
|
|
53
|
+
const value = command ?? json(args);
|
|
54
|
+
return value === undefined ? "" : oneLine(value).slice(0, TOOL_ARGS_GIST_MAX_CHARS);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function json(value: unknown): string | undefined {
|
|
58
|
+
if (value === undefined) return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const serialized = JSON.stringify(value);
|
|
61
|
+
return typeof serialized === "string" ? serialized : undefined;
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function oneLine(value: string): string {
|
|
68
|
+
return value.replaceAll(/\s+/g, " ").trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sameActivity(left: AgentActivity, right: AgentActivity | null): boolean {
|
|
72
|
+
if (right === null || left.type !== right.type) return false;
|
|
73
|
+
if (left.type === "tool" && right.type === "tool") {
|
|
74
|
+
return left.name === right.name && left.argsGist === right.argsGist;
|
|
75
|
+
}
|
|
76
|
+
if (left.type === "retrying" && right.type === "retrying") {
|
|
77
|
+
return left.attempt === right.attempt && left.max === right.max;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
83
|
+
return typeof value === "object" && value !== null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { TSchema } from "typebox";
|
|
2
|
+
import { ASK_OUTPUT_EXTRACTION_POLICY } from "./ask-output.ts";
|
|
3
|
+
|
|
4
|
+
/** A JSON value suitable for deterministic Cassette identity. */
|
|
5
|
+
export type CanonicalJson =
|
|
6
|
+
| null
|
|
7
|
+
| boolean
|
|
8
|
+
| number
|
|
9
|
+
| string
|
|
10
|
+
| CanonicalJson[]
|
|
11
|
+
| CanonicalJsonObject;
|
|
12
|
+
|
|
13
|
+
/** A JSON object with recursively sorted keys. */
|
|
14
|
+
export interface CanonicalJsonObject {
|
|
15
|
+
readonly [key: string]: CanonicalJson;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The conditional behavioral output contract of a structured Ask. */
|
|
19
|
+
export interface AskOutputContract {
|
|
20
|
+
readonly outputSchema: CanonicalJsonObject;
|
|
21
|
+
readonly extractionPolicy: string;
|
|
22
|
+
readonly maxSteers?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Copies a TypeBox schema into recursively key-sorted JSON form without mutating it.
|
|
27
|
+
*
|
|
28
|
+
* Throws when the value cannot be represented in a Cassette JSON artifact.
|
|
29
|
+
*/
|
|
30
|
+
export function canonicalizeSchema(value: unknown): CanonicalJsonObject {
|
|
31
|
+
if (!isObject(value)) throw new TypeError("outputSchema must be a JSON-compatible schema object");
|
|
32
|
+
return canonicalize(value, new Set<object>()) as CanonicalJsonObject;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Builds the recorded and hashed behavioral contract for one structured Ask. */
|
|
36
|
+
export function createAskOutputContract(schema: TSchema, maxSteers?: number): AskOutputContract {
|
|
37
|
+
return {
|
|
38
|
+
outputSchema: canonicalizeSchema(schema),
|
|
39
|
+
extractionPolicy: ASK_OUTPUT_EXTRACTION_POLICY,
|
|
40
|
+
...(maxSteers === undefined ? {} : { maxSteers }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Validates an externally supplied canonical schema representation. */
|
|
45
|
+
export function validateCanonicalOutputSchema(value: unknown): CanonicalJsonObject {
|
|
46
|
+
const canonical = canonicalizeSchema(value);
|
|
47
|
+
if (JSON.stringify(value) !== JSON.stringify(canonical)) {
|
|
48
|
+
throw new TypeError("outputSchema must be in canonical form");
|
|
49
|
+
}
|
|
50
|
+
return canonical;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Validates a policy identifier while allowing future versions to load and diverge at replay. */
|
|
54
|
+
export function validateExtractionPolicy(value: unknown): string {
|
|
55
|
+
if (typeof value !== "string" || !/^[a-z][a-z0-9-]*\/v[1-9][0-9]*$/.test(value)) {
|
|
56
|
+
throw new TypeError("extractionPolicy must be a valid policy identifier");
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function canonicalize(value: unknown, ancestors: Set<object>): CanonicalJson {
|
|
62
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
63
|
+
if (typeof value === "number") {
|
|
64
|
+
if (!Number.isFinite(value)) throw new TypeError("outputSchema must be JSON-compatible");
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
if (ancestors.has(value)) throw new TypeError("outputSchema must be JSON-compatible");
|
|
69
|
+
ancestors.add(value);
|
|
70
|
+
const result = value.map((entry) => canonicalize(entry, ancestors));
|
|
71
|
+
ancestors.delete(value);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
if (!isObject(value)) throw new TypeError("outputSchema must be JSON-compatible");
|
|
75
|
+
if (ancestors.has(value)) throw new TypeError("outputSchema must be JSON-compatible");
|
|
76
|
+
ancestors.add(value);
|
|
77
|
+
const result: Record<string, CanonicalJson> = {};
|
|
78
|
+
for (const key of Object.keys(value).sort()) {
|
|
79
|
+
const entry = canonicalize(value[key], ancestors);
|
|
80
|
+
// JSON Schema defines `required` as a set; TypeBox derives its order from
|
|
81
|
+
// author property insertion, which is not behavioral identity.
|
|
82
|
+
result[key] = key === "required" && Array.isArray(entry) ? sortRequired(entry) : entry;
|
|
83
|
+
}
|
|
84
|
+
ancestors.delete(value);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sortRequired(value: CanonicalJson[]): CanonicalJson[] {
|
|
89
|
+
return value.every((entry) => typeof entry === "string") ? [...value].sort() : value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
93
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
94
|
+
const prototype = Object.getPrototypeOf(value);
|
|
95
|
+
return prototype === Object.prototype || prototype === null;
|
|
96
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AgentActivity, AskOutputChannel, EventSink } from "./events.ts";
|
|
2
|
+
import { agentAskPath, childPath } from "./node-path.ts";
|
|
3
|
+
import type { NodeSnapshot } from "./node-tracker.ts";
|
|
4
|
+
import { promptGist } from "./prompt-gist.ts";
|
|
5
|
+
|
|
6
|
+
/** Emits the four Ask-scoped Lifecycle Events for one exchange. */
|
|
7
|
+
export class AskEvents {
|
|
8
|
+
readonly #emit: EventSink;
|
|
9
|
+
readonly #agent: string;
|
|
10
|
+
readonly #index: number;
|
|
11
|
+
|
|
12
|
+
constructor(emit: EventSink, agent: string, index: number) {
|
|
13
|
+
this.#emit = emit;
|
|
14
|
+
this.#agent = agent;
|
|
15
|
+
this.#index = index;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
start(prompt: string, replayed: boolean): void {
|
|
19
|
+
this.#emit({
|
|
20
|
+
type: "ask_start",
|
|
21
|
+
agent: this.#agent,
|
|
22
|
+
index: this.#index,
|
|
23
|
+
promptGist: promptGist(prompt),
|
|
24
|
+
promptChars: prompt.length,
|
|
25
|
+
...(replayed ? { replayed: true } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
activity = (activity: AgentActivity): void => {
|
|
30
|
+
this.#emit({ type: "ask_activity", agent: this.#agent, index: this.#index, activity });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
output = (channel: AskOutputChannel, text: string): void => {
|
|
34
|
+
this.#emit({ type: "ask_output", agent: this.#agent, index: this.#index, channel, text });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Composes the Nested Node path from this Ask's identity, then reports it. */
|
|
38
|
+
node = (snapshot: NodeSnapshot): void => {
|
|
39
|
+
const path = snapshot.segments.reduce(childPath, agentAskPath(this.#agent, this.#index));
|
|
40
|
+
this.#emit({
|
|
41
|
+
type: "node_update",
|
|
42
|
+
agent: this.#agent,
|
|
43
|
+
path,
|
|
44
|
+
state: snapshot.state,
|
|
45
|
+
...(snapshot.activityGist === undefined ? {} : { activityGist: snapshot.activityGist }),
|
|
46
|
+
...(snapshot.usage === undefined ? {} : { usage: snapshot.usage }),
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
end(durationMs: number, ok: boolean, maxFrameGapMs: number | undefined): void {
|
|
51
|
+
this.#emit({
|
|
52
|
+
type: "ask_end",
|
|
53
|
+
agent: this.#agent,
|
|
54
|
+
index: this.#index,
|
|
55
|
+
durationMs,
|
|
56
|
+
ok,
|
|
57
|
+
...(maxFrameGapMs === undefined ? {} : { maxFrameGapMs }),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EffectiveAskOptions } from "./ask-hash.ts";
|
|
2
|
+
import type { Connection } from "./connection.ts";
|
|
3
|
+
import type { EventSink } from "./events.ts";
|
|
4
|
+
import type { AgentTransport, Frame } from "./transport.ts";
|
|
5
|
+
import type { SpawnOptions } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/** A frame consumer whose lifetime exceeds one Ask; the Agent's AgentUsage satisfies it. */
|
|
8
|
+
export interface FrameObserver {
|
|
9
|
+
observe(frame: Frame): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Options that connect one Ask exchange to its Agent's identity and event stream. */
|
|
13
|
+
export interface AskExchangeOptions {
|
|
14
|
+
readonly agent: string;
|
|
15
|
+
readonly connection: Connection;
|
|
16
|
+
readonly transport: AgentTransport;
|
|
17
|
+
readonly prompt: string;
|
|
18
|
+
readonly index: number;
|
|
19
|
+
readonly ask: EffectiveAskOptions;
|
|
20
|
+
readonly spawnOptions: SpawnOptions;
|
|
21
|
+
readonly definitionName: string | undefined;
|
|
22
|
+
/** Lifecycle Event sink; the exchange emits every Ask-scoped event itself. */
|
|
23
|
+
readonly emit: EventSink;
|
|
24
|
+
/** The Agent's persistent usage accumulator; fed frames only during live Asks. */
|
|
25
|
+
readonly usage: FrameObserver;
|
|
26
|
+
/** Kills the Agent on ASK_TIMEOUT and destructive stalls — the one upward capability. */
|
|
27
|
+
readonly close: () => void;
|
|
28
|
+
/** Test-only override for the fixed duration-limit grace. */
|
|
29
|
+
readonly askLimitGraceMs: number | undefined;
|
|
30
|
+
/** Test-only override for the bounded settle wait after an idle abort. */
|
|
31
|
+
readonly idleAbortSettleMs: number | undefined;
|
|
32
|
+
}
|