omk-agent-core 0.98.1 → 0.98.2
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/harness/agent-harness.d.ts +20 -6
- package/dist/harness/agent-harness.d.ts.map +1 -1
- package/dist/harness/agent-harness.js +300 -307
- package/dist/harness/agent-harness.js.map +1 -1
- package/dist/harness/compaction/operation.d.ts +7 -0
- package/dist/harness/compaction/operation.d.ts.map +1 -1
- package/dist/harness/compaction/operation.js.map +1 -1
- package/dist/harness/harness-session.d.ts +5 -58
- package/dist/harness/harness-session.d.ts.map +1 -1
- package/dist/harness/harness-session.js +15 -18
- package/dist/harness/harness-session.js.map +1 -1
- package/dist/harness/operation-lifecycle-controller.d.ts +68 -0
- package/dist/harness/operation-lifecycle-controller.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-controller.js +199 -0
- package/dist/harness/operation-lifecycle-controller.js.map +1 -0
- package/dist/harness/operation-lifecycle-reducer.d.ts +19 -0
- package/dist/harness/operation-lifecycle-reducer.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-reducer.js +201 -0
- package/dist/harness/operation-lifecycle-reducer.js.map +1 -0
- package/dist/harness/operation-lifecycle-types.d.ts +130 -0
- package/dist/harness/operation-lifecycle-types.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-types.js +34 -0
- package/dist/harness/operation-lifecycle-types.js.map +1 -0
- package/dist/harness/operation-outcome.d.ts +71 -0
- package/dist/harness/operation-outcome.d.ts.map +1 -0
- package/dist/harness/operation-outcome.js +131 -0
- package/dist/harness/operation-outcome.js.map +1 -0
- package/dist/harness/session-write-coordinator.d.ts +76 -0
- package/dist/harness/session-write-coordinator.d.ts.map +1 -0
- package/dist/harness/session-write-coordinator.js +126 -0
- package/dist/harness/session-write-coordinator.js.map +1 -0
- package/dist/harness/subscriber-fanout.d.ts +37 -0
- package/dist/harness/subscriber-fanout.d.ts.map +1 -0
- package/dist/harness/subscriber-fanout.js +73 -0
- package/dist/harness/subscriber-fanout.js.map +1 -0
- package/dist/harness/tree-navigation.d.ts +45 -0
- package/dist/harness/tree-navigation.d.ts.map +1 -0
- package/dist/harness/tree-navigation.js +59 -0
- package/dist/harness/tree-navigation.js.map +1 -0
- package/dist/harness/types.d.ts +34 -3
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -11,11 +11,11 @@ import { AgentHarnessError, toError } from "./errors.js";
|
|
|
11
11
|
export class HarnessSessionFacade {
|
|
12
12
|
session;
|
|
13
13
|
getPhase;
|
|
14
|
-
|
|
15
|
-
constructor(session, getPhase,
|
|
14
|
+
sessionWrites;
|
|
15
|
+
constructor(session, getPhase, sessionWrites) {
|
|
16
16
|
this.session = session;
|
|
17
17
|
this.getPhase = getPhase;
|
|
18
|
-
this.
|
|
18
|
+
this.sessionWrites = sessionWrites;
|
|
19
19
|
}
|
|
20
20
|
async getMetadata() {
|
|
21
21
|
return this.read(() => this.session.getMetadata());
|
|
@@ -42,15 +42,13 @@ export class HarnessSessionFacade {
|
|
|
42
42
|
return this.read(() => this.session.getSessionName());
|
|
43
43
|
}
|
|
44
44
|
getPendingWrites() {
|
|
45
|
-
return
|
|
45
|
+
return this.sessionWrites.snapshot();
|
|
46
46
|
}
|
|
47
47
|
async appendMessage(message) {
|
|
48
|
-
|
|
49
|
-
await this.write(write, () => this.session.appendMessage(write.message));
|
|
48
|
+
await this.write(this.snapshot({ type: "message", message }));
|
|
50
49
|
}
|
|
51
50
|
async appendCustomEntry(customType, data) {
|
|
52
|
-
|
|
53
|
-
await this.write(write, () => this.session.appendCustomEntry(write.customType, write.data));
|
|
51
|
+
await this.write(this.snapshot({ type: "custom", customType, data }));
|
|
54
52
|
}
|
|
55
53
|
async appendCustomMessage(input) {
|
|
56
54
|
const write = this.snapshot({
|
|
@@ -60,25 +58,22 @@ export class HarnessSessionFacade {
|
|
|
60
58
|
display: input.display,
|
|
61
59
|
details: input.details,
|
|
62
60
|
});
|
|
63
|
-
await this.write(write
|
|
61
|
+
await this.write(write);
|
|
64
62
|
}
|
|
65
63
|
async appendLabel(targetId, label) {
|
|
66
64
|
if (!(await this.getEntry(targetId))) {
|
|
67
65
|
throw new AgentHarnessError("invalid_argument", `Entry ${targetId} not found`);
|
|
68
66
|
}
|
|
69
|
-
|
|
70
|
-
await this.write(write, () => this.session.appendLabel(write.targetId, write.label));
|
|
67
|
+
await this.write(this.snapshot({ type: "label", targetId, label }));
|
|
71
68
|
}
|
|
72
69
|
async appendSessionName(name) {
|
|
73
|
-
|
|
74
|
-
await this.write(write, () => this.session.appendSessionName(write.name ?? ""));
|
|
70
|
+
await this.write(this.snapshot({ type: "session_info", name: name.trim() }));
|
|
75
71
|
}
|
|
76
72
|
async setLeafId(targetId) {
|
|
77
73
|
if (targetId !== null && !(await this.getEntry(targetId))) {
|
|
78
74
|
throw new AgentHarnessError("invalid_argument", `Entry ${targetId} not found`);
|
|
79
75
|
}
|
|
80
|
-
|
|
81
|
-
await this.write(write, () => this.session.moveTo(write.targetId));
|
|
76
|
+
await this.write(this.snapshot({ type: "leaf", targetId }));
|
|
82
77
|
}
|
|
83
78
|
async read(operation) {
|
|
84
79
|
try {
|
|
@@ -96,19 +91,21 @@ export class HarnessSessionFacade {
|
|
|
96
91
|
throw new AgentHarnessError("invalid_argument", "Session writes must contain plain data", toError(error));
|
|
97
92
|
}
|
|
98
93
|
}
|
|
99
|
-
async write(write
|
|
94
|
+
async write(write) {
|
|
100
95
|
const phase = this.getPhase();
|
|
101
96
|
if (phase === "turn") {
|
|
102
|
-
this.
|
|
97
|
+
this.sessionWrites.enqueue(write);
|
|
103
98
|
return;
|
|
104
99
|
}
|
|
105
100
|
if (phase !== "idle") {
|
|
106
101
|
throw new AgentHarnessError("invalid_state", `Cannot write session state during ${phase}`);
|
|
107
102
|
}
|
|
108
103
|
try {
|
|
109
|
-
await
|
|
104
|
+
await this.sessionWrites.persistAfterPending(write);
|
|
110
105
|
}
|
|
111
106
|
catch (error) {
|
|
107
|
+
if (error instanceof AgentHarnessError && error.code === "invalid_state")
|
|
108
|
+
throw error;
|
|
112
109
|
throw new AgentHarnessError("session", "Session write failed", toError(error));
|
|
113
110
|
}
|
|
114
111
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness-session.js","sourceRoot":"","sources":["../../src/harness/harness-session.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAkEzD;;;;;;;GAOG;AACH,MAAM,OAAO,oBAAoB;IACf,OAAO,CAAqD;IAC5D,QAAQ,CAAe;IACvB,aAAa,CAA2B;IAEzD,YACC,OAA2D,EAC3D,QAAsB,EACtB,aAAuC,EACtC;QACD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IAAA,CACnC;IAED,KAAK,CAAC,WAAW,GAAiC;QACjD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAAA,CACnD;IAED,KAAK,CAAC,SAAS,GAA2B;QACzC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAAA,CACjD;IAED,KAAK,CAAC,QAAQ,CAAC,EAAU,EAAyC;QACjE,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAAA,CAClD;IAED,KAAK,CAAC,UAAU,GAAyC;QACxD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAAA,CAClD;IAED,KAAK,CAAC,SAAS,CAAC,MAAe,EAAwC;QACtE,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAAA,CACvD;IAED,KAAK,CAAC,YAAY,GAAgC;QACjD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAAA,CACpD;IAED,KAAK,CAAC,QAAQ,CAAC,EAAU,EAA+B;QACvD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAAA,CAClD;IAED,KAAK,CAAC,cAAc,GAAgC;QACnD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAAA,CACtD;IAED,gBAAgB,GAAsC;QACrD,OAAO,uBAAuB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAAA,CACnD;IAED,KAAK,CAAC,aAAa,CAAC,OAAiB,EAAiB;QACrD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;QAC1D,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IAAA,CACzE;IAED,KAAK,CAAC,iBAAiB,CAAC,UAAkB,EAAE,IAAc,EAAiB;QAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAAA,CAC5F;IAED,KAAK,CAAC,mBAAmB,CAAc,KAA4B,EAAiB;QACnF,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC3B,IAAI,EAAE,gBAAgB;YACtB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,OAAO,EAAE,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC;YAC/E,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO;SACtB,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAC5B,IAAI,CAAC,OAAO,CAAC,wBAAwB,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CACpG,CAAC;IAAA,CACF;IAED,KAAK,CAAC,WAAW,CAAC,QAAgB,EAAE,KAAyB,EAAiB;QAC7E,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,iBAAiB,CAAC,kBAAkB,EAAE,SAAS,QAAQ,YAAY,CAAC,CAAC;QAChF,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAAA,CACrF;IAED,KAAK,CAAC,iBAAiB,CAAC,IAAY,EAAiB;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACzE,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;IAAA,CAChF;IAED,KAAK,CAAC,SAAS,CAAC,QAAuB,EAAiB;QACvD,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YAC3D,MAAM,IAAI,iBAAiB,CAAC,kBAAkB,EAAE,SAAS,QAAQ,YAAY,CAAC,CAAC;QAChF,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QACxD,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAAA,CACnE;IAEO,KAAK,CAAC,IAAI,CAAI,SAA2B,EAAc;QAC9D,IAAI,CAAC;YACJ,OAAO,uBAAuB,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC;QACnD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,iBAAiB,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/E,CAAC;IAAA,CACD;IAEO,QAAQ,CAAuC,KAAa,EAAU;QAC7E,IAAI,CAAC;YACJ,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,iBAAiB,CAAC,kBAAkB,EAAE,wCAAwC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3G,CAAC;IAAA,CACD;IAEO,KAAK,CAAC,KAAK,CAAC,KAA4B,EAAE,OAA+B,EAAiB;QACjG,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YACtB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/B,OAAO;QACR,CAAC;QACD,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YACtB,MAAM,IAAI,iBAAiB,CAAC,eAAe,EAAE,qCAAqC,KAAK,EAAE,CAAC,CAAC;QAC5F,CAAC;QACD,IAAI,CAAC;YACJ,MAAM,OAAO,EAAE,CAAC;QACjB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,iBAAiB,CAAC,SAAS,EAAE,sBAAsB,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QAChF,CAAC;IAAA,CACD;CACD","sourcesContent":["import type { ImageContent, TextContent } from \"omk-ai\";\nimport { createImmutableSnapshot } from \"../plain-data.ts\";\nimport { AgentHarnessError, toError } from \"./errors.ts\";\n\ntype FacadeWrite<TMessage> =\n\t| { readonly type: \"message\"; readonly message: TMessage }\n\t| { readonly type: \"custom\"; readonly customType: string; readonly data?: unknown }\n\t| {\n\t\t\treadonly type: \"custom_message\";\n\t\t\treadonly customType: string;\n\t\t\treadonly content: string | (TextContent | ImageContent)[];\n\t\t\treadonly details?: unknown;\n\t\t\treadonly display: boolean;\n\t }\n\t| { readonly type: \"label\"; readonly targetId: string; readonly label: string | undefined }\n\t| { readonly type: \"session_info\"; readonly name?: string }\n\t| { readonly type: \"leaf\"; readonly targetId: string | null };\n\ntype PendingWrite<TMessage> =\n\t| FacadeWrite<TMessage>\n\t| { readonly type: \"thinking_level_change\"; readonly thinkingLevel: string }\n\t| { readonly type: \"model_change\"; readonly provider: string; readonly modelId: string }\n\t| { readonly type: \"active_tools_change\"; readonly activeToolNames: string[] }\n\t| {\n\t\t\treadonly type: \"compaction\";\n\t\t\treadonly summary: string;\n\t\t\treadonly firstKeptEntryId: string;\n\t\t\treadonly tokensBefore: number;\n\t\t\treadonly details?: unknown;\n\t\t\treadonly fromHook?: boolean;\n\t }\n\t| {\n\t\t\treadonly type: \"branch_summary\";\n\t\t\treadonly fromId: string;\n\t\t\treadonly summary: string;\n\t\t\treadonly details?: unknown;\n\t\t\treadonly fromHook?: boolean;\n\t };\n\ninterface SessionPort<TMessage, TEntry, TContext, TMetadata> {\n\tgetMetadata(): Promise<TMetadata>;\n\tgetLeafId(): Promise<string | null>;\n\tgetEntry(id: string): Promise<TEntry | undefined>;\n\tgetEntries(): Promise<TEntry[]>;\n\tgetBranch(fromId?: string): Promise<TEntry[]>;\n\tbuildContext(): Promise<TContext>;\n\tgetLabel(id: string): Promise<string | undefined>;\n\tgetSessionName(): Promise<string | undefined>;\n\tappendMessage(message: TMessage): Promise<string>;\n\tappendCustomEntry(customType: string, data?: unknown): Promise<string>;\n\tappendCustomMessageEntry(\n\t\tcustomType: string,\n\t\tcontent: string | (TextContent | ImageContent)[],\n\t\tdisplay: boolean,\n\t\tdetails?: unknown,\n\t): Promise<string>;\n\tappendLabel(targetId: string, label: string | undefined): Promise<string>;\n\tappendSessionName(name: string): Promise<string>;\n\tmoveTo(entryId: string | null): Promise<string | undefined>;\n}\n\ninterface CustomMessageInput<T = unknown> {\n\treadonly customType: string;\n\treadonly content: string | readonly (TextContent | ImageContent)[];\n\treadonly display: boolean;\n\treadonly details?: T;\n}\n\n/**\n * Internal implementation of the public `HarnessSession` structural interface.\n *\n * This module intentionally imports no harness/session types. `AgentHarness` and the\n * raw session are in a legacy import-cycle component; importing their types here would\n * pull this new boundary into that component because the cycle ratchet treats static\n * type imports as architectural edges. Generic ports keep the implementation a leaf.\n */\nexport class HarnessSessionFacade<TMessage, TEntry, TContext, TMetadata> {\n\tprivate readonly session: SessionPort<TMessage, TEntry, TContext, TMetadata>;\n\tprivate readonly getPhase: () => string;\n\tprivate readonly pendingWrites: PendingWrite<TMessage>[];\n\n\tconstructor(\n\t\tsession: SessionPort<TMessage, TEntry, TContext, TMetadata>,\n\t\tgetPhase: () => string,\n\t\tpendingWrites: PendingWrite<TMessage>[],\n\t) {\n\t\tthis.session = session;\n\t\tthis.getPhase = getPhase;\n\t\tthis.pendingWrites = pendingWrites;\n\t}\n\n\tasync getMetadata(): Promise<Readonly<TMetadata>> {\n\t\treturn this.read(() => this.session.getMetadata());\n\t}\n\n\tasync getLeafId(): Promise<string | null> {\n\t\treturn this.read(() => this.session.getLeafId());\n\t}\n\n\tasync getEntry(id: string): Promise<Readonly<TEntry> | undefined> {\n\t\treturn this.read(() => this.session.getEntry(id));\n\t}\n\n\tasync getEntries(): Promise<readonly Readonly<TEntry>[]> {\n\t\treturn this.read(() => this.session.getEntries());\n\t}\n\n\tasync getBranch(fromId?: string): Promise<readonly Readonly<TEntry>[]> {\n\t\treturn this.read(() => this.session.getBranch(fromId));\n\t}\n\n\tasync buildContext(): Promise<Readonly<TContext>> {\n\t\treturn this.read(() => this.session.buildContext());\n\t}\n\n\tasync getLabel(id: string): Promise<string | undefined> {\n\t\treturn this.read(() => this.session.getLabel(id));\n\t}\n\n\tasync getSessionName(): Promise<string | undefined> {\n\t\treturn this.read(() => this.session.getSessionName());\n\t}\n\n\tgetPendingWrites(): readonly PendingWrite<TMessage>[] {\n\t\treturn createImmutableSnapshot(this.pendingWrites);\n\t}\n\n\tasync appendMessage(message: TMessage): Promise<void> {\n\t\tconst write = this.snapshot({ type: \"message\", message });\n\t\tawait this.write(write, () => this.session.appendMessage(write.message));\n\t}\n\n\tasync appendCustomEntry(customType: string, data?: unknown): Promise<void> {\n\t\tconst write = this.snapshot({ type: \"custom\", customType, data });\n\t\tawait this.write(write, () => this.session.appendCustomEntry(write.customType, write.data));\n\t}\n\n\tasync appendCustomMessage<T = unknown>(input: CustomMessageInput<T>): Promise<void> {\n\t\tconst write = this.snapshot({\n\t\t\ttype: \"custom_message\",\n\t\t\tcustomType: input.customType,\n\t\t\tcontent: typeof input.content === \"string\" ? input.content : [...input.content],\n\t\t\tdisplay: input.display,\n\t\t\tdetails: input.details,\n\t\t});\n\t\tawait this.write(write, () =>\n\t\t\tthis.session.appendCustomMessageEntry(write.customType, write.content, write.display, write.details),\n\t\t);\n\t}\n\n\tasync appendLabel(targetId: string, label: string | undefined): Promise<void> {\n\t\tif (!(await this.getEntry(targetId))) {\n\t\t\tthrow new AgentHarnessError(\"invalid_argument\", `Entry ${targetId} not found`);\n\t\t}\n\t\tconst write = this.snapshot({ type: \"label\", targetId, label });\n\t\tawait this.write(write, () => this.session.appendLabel(write.targetId, write.label));\n\t}\n\n\tasync appendSessionName(name: string): Promise<void> {\n\t\tconst write = this.snapshot({ type: \"session_info\", name: name.trim() });\n\t\tawait this.write(write, () => this.session.appendSessionName(write.name ?? \"\"));\n\t}\n\n\tasync setLeafId(targetId: string | null): Promise<void> {\n\t\tif (targetId !== null && !(await this.getEntry(targetId))) {\n\t\t\tthrow new AgentHarnessError(\"invalid_argument\", `Entry ${targetId} not found`);\n\t\t}\n\t\tconst write = this.snapshot({ type: \"leaf\", targetId });\n\t\tawait this.write(write, () => this.session.moveTo(write.targetId));\n\t}\n\n\tprivate async read<T>(operation: () => Promise<T>): Promise<T> {\n\t\ttry {\n\t\t\treturn createImmutableSnapshot(await operation());\n\t\t} catch (error) {\n\t\t\tthrow new AgentHarnessError(\"session\", \"Session read failed\", toError(error));\n\t\t}\n\t}\n\n\tprivate snapshot<TWrite extends FacadeWrite<TMessage>>(write: TWrite): TWrite {\n\t\ttry {\n\t\t\treturn createImmutableSnapshot(write);\n\t\t} catch (error) {\n\t\t\tthrow new AgentHarnessError(\"invalid_argument\", \"Session writes must contain plain data\", toError(error));\n\t\t}\n\t}\n\n\tprivate async write(write: FacadeWrite<TMessage>, persist: () => Promise<unknown>): Promise<void> {\n\t\tconst phase = this.getPhase();\n\t\tif (phase === \"turn\") {\n\t\t\tthis.pendingWrites.push(write);\n\t\t\treturn;\n\t\t}\n\t\tif (phase !== \"idle\") {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Cannot write session state during ${phase}`);\n\t\t}\n\t\ttry {\n\t\t\tawait persist();\n\t\t} catch (error) {\n\t\t\tthrow new AgentHarnessError(\"session\", \"Session write failed\", toError(error));\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"harness-session.js","sourceRoot":"","sources":["../../src/harness/harness-session.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent, TextContent } from \"omk-ai\";\nimport { createImmutableSnapshot } from \"../plain-data.ts\";\nimport { AgentHarnessError, toError } from \"./errors.ts\";\nimport type { QueuedSessionWrite, SessionWriteCoordinator } from \"./session-write-coordinator.ts\";\n\ntype FacadeWrite<TMessage> =\n\t| { readonly type: \"message\"; readonly message: TMessage }\n\t| { readonly type: \"custom\"; readonly customType: string; readonly data?: unknown }\n\t| {\n\t\t\treadonly type: \"custom_message\";\n\t\t\treadonly customType: string;\n\t\t\treadonly content: string | (TextContent | ImageContent)[];\n\t\t\treadonly details?: unknown;\n\t\t\treadonly display: boolean;\n\t }\n\t| { readonly type: \"label\"; readonly targetId: string; readonly label: string | undefined }\n\t| { readonly type: \"session_info\"; readonly name?: string }\n\t| { readonly type: \"leaf\"; readonly targetId: string | null };\n\ninterface SessionPort<TEntry, TContext, TMetadata> {\n\tgetMetadata(): Promise<TMetadata>;\n\tgetLeafId(): Promise<string | null>;\n\tgetEntry(id: string): Promise<TEntry | undefined>;\n\tgetEntries(): Promise<TEntry[]>;\n\tgetBranch(fromId?: string): Promise<TEntry[]>;\n\tbuildContext(): Promise<TContext>;\n\tgetLabel(id: string): Promise<string | undefined>;\n\tgetSessionName(): Promise<string | undefined>;\n}\n\ninterface CustomMessageInput<T = unknown> {\n\treadonly customType: string;\n\treadonly content: string | readonly (TextContent | ImageContent)[];\n\treadonly display: boolean;\n\treadonly details?: T;\n}\n\n/**\n * Internal implementation of the public `HarnessSession` structural interface.\n *\n * This module intentionally imports no harness/session types. `AgentHarness` and the\n * raw session are in a legacy import-cycle component; importing their types here would\n * pull this new boundary into that component because the cycle ratchet treats static\n * type imports as architectural edges. Generic ports keep the implementation a leaf.\n */\nexport class HarnessSessionFacade<TMessage, TEntry, TContext, TMetadata> {\n\tprivate readonly session: SessionPort<TEntry, TContext, TMetadata>;\n\tprivate readonly getPhase: () => string;\n\tprivate readonly sessionWrites: SessionWriteCoordinator<TMessage>;\n\n\tconstructor(\n\t\tsession: SessionPort<TEntry, TContext, TMetadata>,\n\t\tgetPhase: () => string,\n\t\tsessionWrites: SessionWriteCoordinator<TMessage>,\n\t) {\n\t\tthis.session = session;\n\t\tthis.getPhase = getPhase;\n\t\tthis.sessionWrites = sessionWrites;\n\t}\n\n\tasync getMetadata(): Promise<Readonly<TMetadata>> {\n\t\treturn this.read(() => this.session.getMetadata());\n\t}\n\n\tasync getLeafId(): Promise<string | null> {\n\t\treturn this.read(() => this.session.getLeafId());\n\t}\n\n\tasync getEntry(id: string): Promise<Readonly<TEntry> | undefined> {\n\t\treturn this.read(() => this.session.getEntry(id));\n\t}\n\n\tasync getEntries(): Promise<readonly Readonly<TEntry>[]> {\n\t\treturn this.read(() => this.session.getEntries());\n\t}\n\n\tasync getBranch(fromId?: string): Promise<readonly Readonly<TEntry>[]> {\n\t\treturn this.read(() => this.session.getBranch(fromId));\n\t}\n\n\tasync buildContext(): Promise<Readonly<TContext>> {\n\t\treturn this.read(() => this.session.buildContext());\n\t}\n\n\tasync getLabel(id: string): Promise<string | undefined> {\n\t\treturn this.read(() => this.session.getLabel(id));\n\t}\n\n\tasync getSessionName(): Promise<string | undefined> {\n\t\treturn this.read(() => this.session.getSessionName());\n\t}\n\n\tgetPendingWrites(): readonly QueuedSessionWrite<TMessage>[] {\n\t\treturn this.sessionWrites.snapshot();\n\t}\n\n\tasync appendMessage(message: TMessage): Promise<void> {\n\t\tawait this.write(this.snapshot({ type: \"message\", message }));\n\t}\n\n\tasync appendCustomEntry(customType: string, data?: unknown): Promise<void> {\n\t\tawait this.write(this.snapshot({ type: \"custom\", customType, data }));\n\t}\n\n\tasync appendCustomMessage<T = unknown>(input: CustomMessageInput<T>): Promise<void> {\n\t\tconst write = this.snapshot({\n\t\t\ttype: \"custom_message\",\n\t\t\tcustomType: input.customType,\n\t\t\tcontent: typeof input.content === \"string\" ? input.content : [...input.content],\n\t\t\tdisplay: input.display,\n\t\t\tdetails: input.details,\n\t\t});\n\t\tawait this.write(write);\n\t}\n\n\tasync appendLabel(targetId: string, label: string | undefined): Promise<void> {\n\t\tif (!(await this.getEntry(targetId))) {\n\t\t\tthrow new AgentHarnessError(\"invalid_argument\", `Entry ${targetId} not found`);\n\t\t}\n\t\tawait this.write(this.snapshot({ type: \"label\", targetId, label }));\n\t}\n\n\tasync appendSessionName(name: string): Promise<void> {\n\t\tawait this.write(this.snapshot({ type: \"session_info\", name: name.trim() }));\n\t}\n\n\tasync setLeafId(targetId: string | null): Promise<void> {\n\t\tif (targetId !== null && !(await this.getEntry(targetId))) {\n\t\t\tthrow new AgentHarnessError(\"invalid_argument\", `Entry ${targetId} not found`);\n\t\t}\n\t\tawait this.write(this.snapshot({ type: \"leaf\", targetId }));\n\t}\n\n\tprivate async read<T>(operation: () => Promise<T>): Promise<T> {\n\t\ttry {\n\t\t\treturn createImmutableSnapshot(await operation());\n\t\t} catch (error) {\n\t\t\tthrow new AgentHarnessError(\"session\", \"Session read failed\", toError(error));\n\t\t}\n\t}\n\n\tprivate snapshot<TWrite extends FacadeWrite<TMessage>>(write: TWrite): TWrite {\n\t\ttry {\n\t\t\treturn createImmutableSnapshot(write);\n\t\t} catch (error) {\n\t\t\tthrow new AgentHarnessError(\"invalid_argument\", \"Session writes must contain plain data\", toError(error));\n\t\t}\n\t}\n\n\tprivate async write(write: FacadeWrite<TMessage>): Promise<void> {\n\t\tconst phase = this.getPhase();\n\t\tif (phase === \"turn\") {\n\t\t\tthis.sessionWrites.enqueue(write);\n\t\t\treturn;\n\t\t}\n\t\tif (phase !== \"idle\") {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Cannot write session state during ${phase}`);\n\t\t}\n\t\ttry {\n\t\t\tawait this.sessionWrites.persistAfterPending(write);\n\t\t} catch (error) {\n\t\t\tif (error instanceof AgentHarnessError && error.code === \"invalid_state\") throw error;\n\t\t\tthrow new AgentHarnessError(\"session\", \"Session write failed\", toError(error));\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns the single mutable copy of the harness lifecycle state.
|
|
3
|
+
*
|
|
4
|
+
* The controller is the only writer: it builds commands, runs them through
|
|
5
|
+
* the pure reducer, and performs the side effects the reducer is forbidden
|
|
6
|
+
* from touching — clock reads, ID allocation, abort-signal delivery, and
|
|
7
|
+
* settlement of the operation's `settled` promise.
|
|
8
|
+
*
|
|
9
|
+
* Lease discipline: every mutating method requires the `OperationLease`
|
|
10
|
+
* returned by `begin`. A lease that is not the current record fails closed,
|
|
11
|
+
* so a stale async continuation from a finished operation can never mutate
|
|
12
|
+
* the state of a newer one. `settle()` is the only path from `active` to
|
|
13
|
+
* `idle`, runs the caller's finalizer inside the `settling` barrier, and is
|
|
14
|
+
* guarded against double invocation at both controller and reducer level.
|
|
15
|
+
*
|
|
16
|
+
* The `settled` promise resolves with the recorded outcome and never
|
|
17
|
+
* rejects; finalizer failures surface only through the `settle()` rejection,
|
|
18
|
+
* so callers cannot strand an unhandled rejection by ignoring `lease.settled`.
|
|
19
|
+
*/
|
|
20
|
+
import type { HarnessAttemptOutcome, HarnessAttemptRef, HarnessAttemptSummary, HarnessLifecycleCommand, HarnessLifecycleDependencies, HarnessLifecycleState, HarnessOperationKind, HarnessOperationOutcome, HarnessOperationRef } from "./operation-lifecycle-types.ts";
|
|
21
|
+
export interface OperationLease {
|
|
22
|
+
readonly operation: HarnessOperationRef;
|
|
23
|
+
readonly signal: AbortSignal;
|
|
24
|
+
/** Resolves once with the recorded outcome when the operation finishes settling. Never rejects. */
|
|
25
|
+
readonly settled: Promise<HarnessOperationOutcome>;
|
|
26
|
+
}
|
|
27
|
+
export interface AttemptLease {
|
|
28
|
+
readonly attempt: HarnessAttemptRef;
|
|
29
|
+
/** The owning operation's signal. Attempts never own a separate AbortController. */
|
|
30
|
+
readonly signal: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
export interface HarnessAbortCapture {
|
|
33
|
+
readonly target?: OperationLease;
|
|
34
|
+
readonly signalDelivered: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare class OperationLifecycleController {
|
|
37
|
+
private readonly deps;
|
|
38
|
+
private state;
|
|
39
|
+
private current?;
|
|
40
|
+
private readonly idleWaiters;
|
|
41
|
+
constructor(deps: HarnessLifecycleDependencies);
|
|
42
|
+
getSnapshot(): Readonly<HarnessLifecycleState>;
|
|
43
|
+
getCurrentOperation(): HarnessOperationRef | undefined;
|
|
44
|
+
begin(kind: HarnessOperationKind): OperationLease;
|
|
45
|
+
setStage(lease: OperationLease, stage: Extract<HarnessLifecycleCommand, {
|
|
46
|
+
type: "stage";
|
|
47
|
+
}>["stage"]): void;
|
|
48
|
+
beginAttempt(lease: OperationLease, reason: HarnessAttemptRef["reason"]): AttemptLease;
|
|
49
|
+
finishAttempt(lease: OperationLease, attempt: AttemptLease, outcome: HarnessAttemptOutcome): void;
|
|
50
|
+
/** Captures the current operation as the abort target; never targets operations started later. */
|
|
51
|
+
requestAbort(): HarnessAbortCapture;
|
|
52
|
+
/**
|
|
53
|
+
* Sole active -> idle path. Runs `finalize` inside the settling barrier, then
|
|
54
|
+
* releases state and resolves the lease. A finalizer failure still completes
|
|
55
|
+
* the state release and resolves `lease.settled`, then rejects this call.
|
|
56
|
+
*/
|
|
57
|
+
settle(lease: OperationLease, outcome: HarnessOperationOutcome, finalize: () => Promise<void>): Promise<HarnessOperationOutcome>;
|
|
58
|
+
/** Bounded attempt summaries for the current operation; no transcripts. */
|
|
59
|
+
getAttemptSummaries(lease: OperationLease): readonly HarnessAttemptSummary[];
|
|
60
|
+
/** The recorded summary for one attempt of the current operation. */
|
|
61
|
+
getAttemptSummary(lease: OperationLease, attempt: AttemptLease): HarnessAttemptSummary;
|
|
62
|
+
waitForIdle(): Promise<void>;
|
|
63
|
+
private expectActive;
|
|
64
|
+
private expectCurrentLease;
|
|
65
|
+
private apply;
|
|
66
|
+
private asHarnessError;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=operation-lifecycle-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-lifecycle-controller.d.ts","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,KAAK,EACX,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,4BAA4B,EAC5B,qBAAqB,EAErB,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,MAAM,gCAAgC,CAAC;AAExC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,mGAAmG;IACnG,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC;AA4BD,qBAAa,4BAA4B;IACxC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IACpD,OAAO,CAAC,KAAK,CAA+E;IAC5F,OAAO,CAAC,OAAO,CAAC,CAAkB;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IAErD,YAAY,IAAI,EAAE,4BAA4B,EAE7C;IAED,WAAW,IAAI,QAAQ,CAAC,qBAAqB,CAAC,CAE7C;IAED,mBAAmB,IAAI,mBAAmB,GAAG,SAAS,CAErD;IAED,KAAK,CAAC,IAAI,EAAE,oBAAoB,GAAG,cAAc,CAmBhD;IAED,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,CAAC,uBAAuB,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,OAAO,CAAC,GAAG,IAAI,CAGzG;IAED,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,GAAG,YAAY,CAarF;IAED,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAIhG;IAED,kGAAkG;IAClG,YAAY,IAAI,mBAAmB,CAWlC;IAED;;;;OAIG;IACG,MAAM,CACX,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC3B,OAAO,CAAC,uBAAuB,CAAC,CA0BlC;IAED,2EAA2E;IAC3E,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,qBAAqB,EAAE,CAW3E;IAED,qEAAqE;IACrE,iBAAiB,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,GAAG,qBAAqB,CAOrF;IAEK,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAKjC;IAED,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,cAAc;CAGtB","sourcesContent":["/**\n * Owns the single mutable copy of the harness lifecycle state.\n *\n * The controller is the only writer: it builds commands, runs them through\n * the pure reducer, and performs the side effects the reducer is forbidden\n * from touching — clock reads, ID allocation, abort-signal delivery, and\n * settlement of the operation's `settled` promise.\n *\n * Lease discipline: every mutating method requires the `OperationLease`\n * returned by `begin`. A lease that is not the current record fails closed,\n * so a stale async continuation from a finished operation can never mutate\n * the state of a newer one. `settle()` is the only path from `active` to\n * `idle`, runs the caller's finalizer inside the `settling` barrier, and is\n * guarded against double invocation at both controller and reducer level.\n *\n * The `settled` promise resolves with the recorded outcome and never\n * rejects; finalizer failures surface only through the `settle()` rejection,\n * so callers cannot strand an unhandled rejection by ignoring `lease.settled`.\n */\n\nimport { createImmutableSnapshot } from \"../plain-data.ts\";\nimport { AgentHarnessError } from \"./errors.ts\";\nimport { initialHarnessLifecycleState, reduceHarnessLifecycle } from \"./operation-lifecycle-reducer.ts\";\nimport type {\n\tHarnessAttemptOutcome,\n\tHarnessAttemptRef,\n\tHarnessAttemptSummary,\n\tHarnessLifecycleCommand,\n\tHarnessLifecycleDependencies,\n\tHarnessLifecycleState,\n\tHarnessLifecycleViolation,\n\tHarnessOperationKind,\n\tHarnessOperationOutcome,\n\tHarnessOperationRef,\n} from \"./operation-lifecycle-types.ts\";\n\nexport interface OperationLease {\n\treadonly operation: HarnessOperationRef;\n\treadonly signal: AbortSignal;\n\t/** Resolves once with the recorded outcome when the operation finishes settling. Never rejects. */\n\treadonly settled: Promise<HarnessOperationOutcome>;\n}\n\nexport interface AttemptLease {\n\treadonly attempt: HarnessAttemptRef;\n\t/** The owning operation's signal. Attempts never own a separate AbortController. */\n\treadonly signal: AbortSignal;\n}\n\nexport interface HarnessAbortCapture {\n\treadonly target?: OperationLease;\n\treadonly signalDelivered: boolean;\n}\n\ninterface OperationRecord {\n\treadonly lease: OperationLease;\n\treadonly controller: AbortController;\n\treadonly attemptFinishTimes: Map<string, number>;\n\tsettleStarted: boolean;\n\tresolveSettled: (outcome: HarnessOperationOutcome) => void;\n}\n\n/**\n * Deep-freeze a reducer-produced state in place.\n *\n * Operation and attempt refs are already frozen at creation, and the reducer\n * allocates fresh state objects per transition, so this is O(attempts) per\n * transition rather than a clone. That keeps `getSnapshot()` free to hand out\n * the live reference: `readonly` is compile-time only, but a frozen object\n * resists `Reflect.set` from extension code at runtime too.\n */\nfunction freezeLifecycleState(state: HarnessLifecycleState): HarnessLifecycleState {\n\tif (state.tag !== \"idle\") {\n\t\tfor (const settled of state.attempts) Object.freeze(settled);\n\t\tObject.freeze(state.attempts);\n\t}\n\tif (state.tag === \"settling\") Object.freeze(state.outcome);\n\treturn Object.freeze(state);\n}\n\nexport class OperationLifecycleController {\n\tprivate readonly deps: HarnessLifecycleDependencies;\n\tprivate state: HarnessLifecycleState = freezeLifecycleState(initialHarnessLifecycleState());\n\tprivate current?: OperationRecord;\n\tprivate readonly idleWaiters = new Set<() => void>();\n\n\tconstructor(deps: HarnessLifecycleDependencies) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetSnapshot(): Readonly<HarnessLifecycleState> {\n\t\treturn this.state;\n\t}\n\n\tgetCurrentOperation(): HarnessOperationRef | undefined {\n\t\treturn this.state.tag === \"idle\" ? undefined : this.state.operation;\n\t}\n\n\tbegin(kind: HarnessOperationKind): OperationLease {\n\t\tconst sequence = this.state.tag === \"idle\" ? this.state.lastSequence + 1 : -1;\n\t\t// Frozen once here so every downstream copy — lease, getters, and event\n\t\t// payloads — shares one identity no listener can rewrite.\n\t\tconst operation = createImmutableSnapshot<HarnessOperationRef>({\n\t\t\toperationId: this.deps.createOperationId(),\n\t\t\tsequence,\n\t\t\tkind,\n\t\t\tstartedAtMs: this.deps.now(),\n\t\t});\n\t\tthis.apply({ type: \"begin\", operation });\n\t\tconst controller = new AbortController();\n\t\tlet resolveSettled: (outcome: HarnessOperationOutcome) => void = () => undefined;\n\t\tconst settled = new Promise<HarnessOperationOutcome>((resolve) => {\n\t\t\tresolveSettled = resolve;\n\t\t});\n\t\tconst lease: OperationLease = { operation, signal: controller.signal, settled };\n\t\tthis.current = { lease, controller, attemptFinishTimes: new Map(), settleStarted: false, resolveSettled };\n\t\treturn lease;\n\t}\n\n\tsetStage(lease: OperationLease, stage: Extract<HarnessLifecycleCommand, { type: \"stage\" }>[\"stage\"]): void {\n\t\tthis.expectCurrentLease(lease);\n\t\tthis.apply({ type: \"stage\", operationId: lease.operation.operationId, stage });\n\t}\n\n\tbeginAttempt(lease: OperationLease, reason: HarnessAttemptRef[\"reason\"]): AttemptLease {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tconst active = this.expectActive();\n\t\tconst attempt = createImmutableSnapshot<HarnessAttemptRef>({\n\t\t\toperationId: lease.operation.operationId,\n\t\t\tattemptId: `${lease.operation.operationId}:a${active.attempts.length}`,\n\t\t\tindex: active.attempts.length,\n\t\t\treason,\n\t\t\tstartedAtMs: this.deps.now(),\n\t\t});\n\t\tthis.apply({ type: \"attempt_begin\", attempt });\n\t\trecord.attemptFinishTimes.delete(attempt.attemptId);\n\t\treturn { attempt, signal: lease.signal };\n\t}\n\n\tfinishAttempt(lease: OperationLease, attempt: AttemptLease, outcome: HarnessAttemptOutcome): void {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tthis.apply({ type: \"attempt_end\", attemptId: attempt.attempt.attemptId, outcome });\n\t\trecord.attemptFinishTimes.set(attempt.attempt.attemptId, this.deps.now());\n\t}\n\n\t/** Captures the current operation as the abort target; never targets operations started later. */\n\trequestAbort(): HarnessAbortCapture {\n\t\tconst record = this.current;\n\t\tif (record === undefined || this.state.tag === \"idle\") {\n\t\t\treturn { signalDelivered: false };\n\t\t}\n\t\tif (this.state.tag === \"settling\") {\n\t\t\treturn { target: record.lease, signalDelivered: false };\n\t\t}\n\t\tthis.apply({ type: \"abort_request\", operationId: record.lease.operation.operationId });\n\t\tif (!record.controller.signal.aborted) record.controller.abort();\n\t\treturn { target: record.lease, signalDelivered: true };\n\t}\n\n\t/**\n\t * Sole active -> idle path. Runs `finalize` inside the settling barrier, then\n\t * releases state and resolves the lease. A finalizer failure still completes\n\t * the state release and resolves `lease.settled`, then rejects this call.\n\t */\n\tasync settle(\n\t\tlease: OperationLease,\n\t\toutcome: HarnessOperationOutcome,\n\t\tfinalize: () => Promise<void>,\n\t): Promise<HarnessOperationOutcome> {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tif (record.settleStarted) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Operation ${lease.operation.operationId} is already settling`);\n\t\t}\n\t\t// A rejected settle_begin (e.g. an attempt still open) is not a settlement.\n\t\t// Marking it as one would wedge the operation: every later settle() would\n\t\t// report \"already settling\" while the state never left `active`, and\n\t\t// `lease.settled` plus every waitForIdle() waiter would hang forever.\n\t\tthis.apply({ type: \"settle_begin\", operationId: lease.operation.operationId, outcome });\n\t\trecord.settleStarted = true;\n\t\tlet finalizeError: unknown;\n\t\ttry {\n\t\t\tawait finalize();\n\t\t} catch (error) {\n\t\t\tfinalizeError = error;\n\t\t}\n\t\tthis.apply({ type: \"settle_finish\", operationId: lease.operation.operationId });\n\t\tthis.current = undefined;\n\t\trecord.resolveSettled(outcome);\n\t\tfor (const release of [...this.idleWaiters]) {\n\t\t\tthis.idleWaiters.delete(release);\n\t\t\trelease();\n\t\t}\n\t\tif (finalizeError !== undefined) throw finalizeError;\n\t\treturn outcome;\n\t}\n\n\t/** Bounded attempt summaries for the current operation; no transcripts. */\n\tgetAttemptSummaries(lease: OperationLease): readonly HarnessAttemptSummary[] {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tconst attempts = this.state.tag === \"idle\" ? [] : this.state.attempts;\n\t\treturn attempts.map(({ attempt, outcome }) => ({\n\t\t\tattemptId: attempt.attemptId,\n\t\t\tindex: attempt.index,\n\t\t\treason: attempt.reason,\n\t\t\toutcome,\n\t\t\tstartedAtMs: attempt.startedAtMs,\n\t\t\tfinishedAtMs: record.attemptFinishTimes.get(attempt.attemptId) ?? attempt.startedAtMs,\n\t\t}));\n\t}\n\n\t/** The recorded summary for one attempt of the current operation. */\n\tgetAttemptSummary(lease: OperationLease, attempt: AttemptLease): HarnessAttemptSummary {\n\t\tconst attemptId = attempt.attempt.attemptId;\n\t\tconst summary = this.getAttemptSummaries(lease).find((candidate) => candidate.attemptId === attemptId);\n\t\tif (summary === undefined) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `No summary recorded for attempt ${attemptId}`);\n\t\t}\n\t\treturn summary;\n\t}\n\n\tasync waitForIdle(): Promise<void> {\n\t\tif (this.state.tag === \"idle\") return;\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tthis.idleWaiters.add(resolve);\n\t\t});\n\t}\n\n\tprivate expectActive(): Extract<HarnessLifecycleState, { tag: \"active\" }> {\n\t\tif (this.state.tag !== \"active\") {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Expected an active operation, state is ${this.state.tag}`);\n\t\t}\n\t\treturn this.state;\n\t}\n\n\tprivate expectCurrentLease(lease: OperationLease): OperationRecord {\n\t\tconst record = this.current;\n\t\tif (record === undefined || record.lease !== lease) {\n\t\t\tthrow new AgentHarnessError(\n\t\t\t\t\"invalid_state\",\n\t\t\t\t`Stale operation lease for ${lease.operation.operationId}; the operation no longer owns lifecycle state`,\n\t\t\t);\n\t\t}\n\t\treturn record;\n\t}\n\n\tprivate apply(command: HarnessLifecycleCommand): void {\n\t\tconst next = reduceHarnessLifecycle(this.state, command);\n\t\tif (!next.ok) throw this.asHarnessError(next.error);\n\t\tthis.state = freezeLifecycleState(next.value);\n\t}\n\n\tprivate asHarnessError(violation: HarnessLifecycleViolation): AgentHarnessError {\n\t\treturn new AgentHarnessError(violation.code === \"busy\" ? \"busy\" : \"invalid_state\", violation.message, violation);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns the single mutable copy of the harness lifecycle state.
|
|
3
|
+
*
|
|
4
|
+
* The controller is the only writer: it builds commands, runs them through
|
|
5
|
+
* the pure reducer, and performs the side effects the reducer is forbidden
|
|
6
|
+
* from touching — clock reads, ID allocation, abort-signal delivery, and
|
|
7
|
+
* settlement of the operation's `settled` promise.
|
|
8
|
+
*
|
|
9
|
+
* Lease discipline: every mutating method requires the `OperationLease`
|
|
10
|
+
* returned by `begin`. A lease that is not the current record fails closed,
|
|
11
|
+
* so a stale async continuation from a finished operation can never mutate
|
|
12
|
+
* the state of a newer one. `settle()` is the only path from `active` to
|
|
13
|
+
* `idle`, runs the caller's finalizer inside the `settling` barrier, and is
|
|
14
|
+
* guarded against double invocation at both controller and reducer level.
|
|
15
|
+
*
|
|
16
|
+
* The `settled` promise resolves with the recorded outcome and never
|
|
17
|
+
* rejects; finalizer failures surface only through the `settle()` rejection,
|
|
18
|
+
* so callers cannot strand an unhandled rejection by ignoring `lease.settled`.
|
|
19
|
+
*/
|
|
20
|
+
import { createImmutableSnapshot } from "../plain-data.js";
|
|
21
|
+
import { AgentHarnessError } from "./errors.js";
|
|
22
|
+
import { initialHarnessLifecycleState, reduceHarnessLifecycle } from "./operation-lifecycle-reducer.js";
|
|
23
|
+
/**
|
|
24
|
+
* Deep-freeze a reducer-produced state in place.
|
|
25
|
+
*
|
|
26
|
+
* Operation and attempt refs are already frozen at creation, and the reducer
|
|
27
|
+
* allocates fresh state objects per transition, so this is O(attempts) per
|
|
28
|
+
* transition rather than a clone. That keeps `getSnapshot()` free to hand out
|
|
29
|
+
* the live reference: `readonly` is compile-time only, but a frozen object
|
|
30
|
+
* resists `Reflect.set` from extension code at runtime too.
|
|
31
|
+
*/
|
|
32
|
+
function freezeLifecycleState(state) {
|
|
33
|
+
if (state.tag !== "idle") {
|
|
34
|
+
for (const settled of state.attempts)
|
|
35
|
+
Object.freeze(settled);
|
|
36
|
+
Object.freeze(state.attempts);
|
|
37
|
+
}
|
|
38
|
+
if (state.tag === "settling")
|
|
39
|
+
Object.freeze(state.outcome);
|
|
40
|
+
return Object.freeze(state);
|
|
41
|
+
}
|
|
42
|
+
export class OperationLifecycleController {
|
|
43
|
+
deps;
|
|
44
|
+
state = freezeLifecycleState(initialHarnessLifecycleState());
|
|
45
|
+
current;
|
|
46
|
+
idleWaiters = new Set();
|
|
47
|
+
constructor(deps) {
|
|
48
|
+
this.deps = deps;
|
|
49
|
+
}
|
|
50
|
+
getSnapshot() {
|
|
51
|
+
return this.state;
|
|
52
|
+
}
|
|
53
|
+
getCurrentOperation() {
|
|
54
|
+
return this.state.tag === "idle" ? undefined : this.state.operation;
|
|
55
|
+
}
|
|
56
|
+
begin(kind) {
|
|
57
|
+
const sequence = this.state.tag === "idle" ? this.state.lastSequence + 1 : -1;
|
|
58
|
+
// Frozen once here so every downstream copy — lease, getters, and event
|
|
59
|
+
// payloads — shares one identity no listener can rewrite.
|
|
60
|
+
const operation = createImmutableSnapshot({
|
|
61
|
+
operationId: this.deps.createOperationId(),
|
|
62
|
+
sequence,
|
|
63
|
+
kind,
|
|
64
|
+
startedAtMs: this.deps.now(),
|
|
65
|
+
});
|
|
66
|
+
this.apply({ type: "begin", operation });
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
let resolveSettled = () => undefined;
|
|
69
|
+
const settled = new Promise((resolve) => {
|
|
70
|
+
resolveSettled = resolve;
|
|
71
|
+
});
|
|
72
|
+
const lease = { operation, signal: controller.signal, settled };
|
|
73
|
+
this.current = { lease, controller, attemptFinishTimes: new Map(), settleStarted: false, resolveSettled };
|
|
74
|
+
return lease;
|
|
75
|
+
}
|
|
76
|
+
setStage(lease, stage) {
|
|
77
|
+
this.expectCurrentLease(lease);
|
|
78
|
+
this.apply({ type: "stage", operationId: lease.operation.operationId, stage });
|
|
79
|
+
}
|
|
80
|
+
beginAttempt(lease, reason) {
|
|
81
|
+
const record = this.expectCurrentLease(lease);
|
|
82
|
+
const active = this.expectActive();
|
|
83
|
+
const attempt = createImmutableSnapshot({
|
|
84
|
+
operationId: lease.operation.operationId,
|
|
85
|
+
attemptId: `${lease.operation.operationId}:a${active.attempts.length}`,
|
|
86
|
+
index: active.attempts.length,
|
|
87
|
+
reason,
|
|
88
|
+
startedAtMs: this.deps.now(),
|
|
89
|
+
});
|
|
90
|
+
this.apply({ type: "attempt_begin", attempt });
|
|
91
|
+
record.attemptFinishTimes.delete(attempt.attemptId);
|
|
92
|
+
return { attempt, signal: lease.signal };
|
|
93
|
+
}
|
|
94
|
+
finishAttempt(lease, attempt, outcome) {
|
|
95
|
+
const record = this.expectCurrentLease(lease);
|
|
96
|
+
this.apply({ type: "attempt_end", attemptId: attempt.attempt.attemptId, outcome });
|
|
97
|
+
record.attemptFinishTimes.set(attempt.attempt.attemptId, this.deps.now());
|
|
98
|
+
}
|
|
99
|
+
/** Captures the current operation as the abort target; never targets operations started later. */
|
|
100
|
+
requestAbort() {
|
|
101
|
+
const record = this.current;
|
|
102
|
+
if (record === undefined || this.state.tag === "idle") {
|
|
103
|
+
return { signalDelivered: false };
|
|
104
|
+
}
|
|
105
|
+
if (this.state.tag === "settling") {
|
|
106
|
+
return { target: record.lease, signalDelivered: false };
|
|
107
|
+
}
|
|
108
|
+
this.apply({ type: "abort_request", operationId: record.lease.operation.operationId });
|
|
109
|
+
if (!record.controller.signal.aborted)
|
|
110
|
+
record.controller.abort();
|
|
111
|
+
return { target: record.lease, signalDelivered: true };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sole active -> idle path. Runs `finalize` inside the settling barrier, then
|
|
115
|
+
* releases state and resolves the lease. A finalizer failure still completes
|
|
116
|
+
* the state release and resolves `lease.settled`, then rejects this call.
|
|
117
|
+
*/
|
|
118
|
+
async settle(lease, outcome, finalize) {
|
|
119
|
+
const record = this.expectCurrentLease(lease);
|
|
120
|
+
if (record.settleStarted) {
|
|
121
|
+
throw new AgentHarnessError("invalid_state", `Operation ${lease.operation.operationId} is already settling`);
|
|
122
|
+
}
|
|
123
|
+
// A rejected settle_begin (e.g. an attempt still open) is not a settlement.
|
|
124
|
+
// Marking it as one would wedge the operation: every later settle() would
|
|
125
|
+
// report "already settling" while the state never left `active`, and
|
|
126
|
+
// `lease.settled` plus every waitForIdle() waiter would hang forever.
|
|
127
|
+
this.apply({ type: "settle_begin", operationId: lease.operation.operationId, outcome });
|
|
128
|
+
record.settleStarted = true;
|
|
129
|
+
let finalizeError;
|
|
130
|
+
try {
|
|
131
|
+
await finalize();
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
finalizeError = error;
|
|
135
|
+
}
|
|
136
|
+
this.apply({ type: "settle_finish", operationId: lease.operation.operationId });
|
|
137
|
+
this.current = undefined;
|
|
138
|
+
record.resolveSettled(outcome);
|
|
139
|
+
for (const release of [...this.idleWaiters]) {
|
|
140
|
+
this.idleWaiters.delete(release);
|
|
141
|
+
release();
|
|
142
|
+
}
|
|
143
|
+
if (finalizeError !== undefined)
|
|
144
|
+
throw finalizeError;
|
|
145
|
+
return outcome;
|
|
146
|
+
}
|
|
147
|
+
/** Bounded attempt summaries for the current operation; no transcripts. */
|
|
148
|
+
getAttemptSummaries(lease) {
|
|
149
|
+
const record = this.expectCurrentLease(lease);
|
|
150
|
+
const attempts = this.state.tag === "idle" ? [] : this.state.attempts;
|
|
151
|
+
return attempts.map(({ attempt, outcome }) => ({
|
|
152
|
+
attemptId: attempt.attemptId,
|
|
153
|
+
index: attempt.index,
|
|
154
|
+
reason: attempt.reason,
|
|
155
|
+
outcome,
|
|
156
|
+
startedAtMs: attempt.startedAtMs,
|
|
157
|
+
finishedAtMs: record.attemptFinishTimes.get(attempt.attemptId) ?? attempt.startedAtMs,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/** The recorded summary for one attempt of the current operation. */
|
|
161
|
+
getAttemptSummary(lease, attempt) {
|
|
162
|
+
const attemptId = attempt.attempt.attemptId;
|
|
163
|
+
const summary = this.getAttemptSummaries(lease).find((candidate) => candidate.attemptId === attemptId);
|
|
164
|
+
if (summary === undefined) {
|
|
165
|
+
throw new AgentHarnessError("invalid_state", `No summary recorded for attempt ${attemptId}`);
|
|
166
|
+
}
|
|
167
|
+
return summary;
|
|
168
|
+
}
|
|
169
|
+
async waitForIdle() {
|
|
170
|
+
if (this.state.tag === "idle")
|
|
171
|
+
return;
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
this.idleWaiters.add(resolve);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
expectActive() {
|
|
177
|
+
if (this.state.tag !== "active") {
|
|
178
|
+
throw new AgentHarnessError("invalid_state", `Expected an active operation, state is ${this.state.tag}`);
|
|
179
|
+
}
|
|
180
|
+
return this.state;
|
|
181
|
+
}
|
|
182
|
+
expectCurrentLease(lease) {
|
|
183
|
+
const record = this.current;
|
|
184
|
+
if (record === undefined || record.lease !== lease) {
|
|
185
|
+
throw new AgentHarnessError("invalid_state", `Stale operation lease for ${lease.operation.operationId}; the operation no longer owns lifecycle state`);
|
|
186
|
+
}
|
|
187
|
+
return record;
|
|
188
|
+
}
|
|
189
|
+
apply(command) {
|
|
190
|
+
const next = reduceHarnessLifecycle(this.state, command);
|
|
191
|
+
if (!next.ok)
|
|
192
|
+
throw this.asHarnessError(next.error);
|
|
193
|
+
this.state = freezeLifecycleState(next.value);
|
|
194
|
+
}
|
|
195
|
+
asHarnessError(violation) {
|
|
196
|
+
return new AgentHarnessError(violation.code === "busy" ? "busy" : "invalid_state", violation.message, violation);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=operation-lifecycle-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-lifecycle-controller.js","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Owns the single mutable copy of the harness lifecycle state.\n *\n * The controller is the only writer: it builds commands, runs them through\n * the pure reducer, and performs the side effects the reducer is forbidden\n * from touching — clock reads, ID allocation, abort-signal delivery, and\n * settlement of the operation's `settled` promise.\n *\n * Lease discipline: every mutating method requires the `OperationLease`\n * returned by `begin`. A lease that is not the current record fails closed,\n * so a stale async continuation from a finished operation can never mutate\n * the state of a newer one. `settle()` is the only path from `active` to\n * `idle`, runs the caller's finalizer inside the `settling` barrier, and is\n * guarded against double invocation at both controller and reducer level.\n *\n * The `settled` promise resolves with the recorded outcome and never\n * rejects; finalizer failures surface only through the `settle()` rejection,\n * so callers cannot strand an unhandled rejection by ignoring `lease.settled`.\n */\n\nimport { createImmutableSnapshot } from \"../plain-data.ts\";\nimport { AgentHarnessError } from \"./errors.ts\";\nimport { initialHarnessLifecycleState, reduceHarnessLifecycle } from \"./operation-lifecycle-reducer.ts\";\nimport type {\n\tHarnessAttemptOutcome,\n\tHarnessAttemptRef,\n\tHarnessAttemptSummary,\n\tHarnessLifecycleCommand,\n\tHarnessLifecycleDependencies,\n\tHarnessLifecycleState,\n\tHarnessLifecycleViolation,\n\tHarnessOperationKind,\n\tHarnessOperationOutcome,\n\tHarnessOperationRef,\n} from \"./operation-lifecycle-types.ts\";\n\nexport interface OperationLease {\n\treadonly operation: HarnessOperationRef;\n\treadonly signal: AbortSignal;\n\t/** Resolves once with the recorded outcome when the operation finishes settling. Never rejects. */\n\treadonly settled: Promise<HarnessOperationOutcome>;\n}\n\nexport interface AttemptLease {\n\treadonly attempt: HarnessAttemptRef;\n\t/** The owning operation's signal. Attempts never own a separate AbortController. */\n\treadonly signal: AbortSignal;\n}\n\nexport interface HarnessAbortCapture {\n\treadonly target?: OperationLease;\n\treadonly signalDelivered: boolean;\n}\n\ninterface OperationRecord {\n\treadonly lease: OperationLease;\n\treadonly controller: AbortController;\n\treadonly attemptFinishTimes: Map<string, number>;\n\tsettleStarted: boolean;\n\tresolveSettled: (outcome: HarnessOperationOutcome) => void;\n}\n\n/**\n * Deep-freeze a reducer-produced state in place.\n *\n * Operation and attempt refs are already frozen at creation, and the reducer\n * allocates fresh state objects per transition, so this is O(attempts) per\n * transition rather than a clone. That keeps `getSnapshot()` free to hand out\n * the live reference: `readonly` is compile-time only, but a frozen object\n * resists `Reflect.set` from extension code at runtime too.\n */\nfunction freezeLifecycleState(state: HarnessLifecycleState): HarnessLifecycleState {\n\tif (state.tag !== \"idle\") {\n\t\tfor (const settled of state.attempts) Object.freeze(settled);\n\t\tObject.freeze(state.attempts);\n\t}\n\tif (state.tag === \"settling\") Object.freeze(state.outcome);\n\treturn Object.freeze(state);\n}\n\nexport class OperationLifecycleController {\n\tprivate readonly deps: HarnessLifecycleDependencies;\n\tprivate state: HarnessLifecycleState = freezeLifecycleState(initialHarnessLifecycleState());\n\tprivate current?: OperationRecord;\n\tprivate readonly idleWaiters = new Set<() => void>();\n\n\tconstructor(deps: HarnessLifecycleDependencies) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetSnapshot(): Readonly<HarnessLifecycleState> {\n\t\treturn this.state;\n\t}\n\n\tgetCurrentOperation(): HarnessOperationRef | undefined {\n\t\treturn this.state.tag === \"idle\" ? undefined : this.state.operation;\n\t}\n\n\tbegin(kind: HarnessOperationKind): OperationLease {\n\t\tconst sequence = this.state.tag === \"idle\" ? this.state.lastSequence + 1 : -1;\n\t\t// Frozen once here so every downstream copy — lease, getters, and event\n\t\t// payloads — shares one identity no listener can rewrite.\n\t\tconst operation = createImmutableSnapshot<HarnessOperationRef>({\n\t\t\toperationId: this.deps.createOperationId(),\n\t\t\tsequence,\n\t\t\tkind,\n\t\t\tstartedAtMs: this.deps.now(),\n\t\t});\n\t\tthis.apply({ type: \"begin\", operation });\n\t\tconst controller = new AbortController();\n\t\tlet resolveSettled: (outcome: HarnessOperationOutcome) => void = () => undefined;\n\t\tconst settled = new Promise<HarnessOperationOutcome>((resolve) => {\n\t\t\tresolveSettled = resolve;\n\t\t});\n\t\tconst lease: OperationLease = { operation, signal: controller.signal, settled };\n\t\tthis.current = { lease, controller, attemptFinishTimes: new Map(), settleStarted: false, resolveSettled };\n\t\treturn lease;\n\t}\n\n\tsetStage(lease: OperationLease, stage: Extract<HarnessLifecycleCommand, { type: \"stage\" }>[\"stage\"]): void {\n\t\tthis.expectCurrentLease(lease);\n\t\tthis.apply({ type: \"stage\", operationId: lease.operation.operationId, stage });\n\t}\n\n\tbeginAttempt(lease: OperationLease, reason: HarnessAttemptRef[\"reason\"]): AttemptLease {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tconst active = this.expectActive();\n\t\tconst attempt = createImmutableSnapshot<HarnessAttemptRef>({\n\t\t\toperationId: lease.operation.operationId,\n\t\t\tattemptId: `${lease.operation.operationId}:a${active.attempts.length}`,\n\t\t\tindex: active.attempts.length,\n\t\t\treason,\n\t\t\tstartedAtMs: this.deps.now(),\n\t\t});\n\t\tthis.apply({ type: \"attempt_begin\", attempt });\n\t\trecord.attemptFinishTimes.delete(attempt.attemptId);\n\t\treturn { attempt, signal: lease.signal };\n\t}\n\n\tfinishAttempt(lease: OperationLease, attempt: AttemptLease, outcome: HarnessAttemptOutcome): void {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tthis.apply({ type: \"attempt_end\", attemptId: attempt.attempt.attemptId, outcome });\n\t\trecord.attemptFinishTimes.set(attempt.attempt.attemptId, this.deps.now());\n\t}\n\n\t/** Captures the current operation as the abort target; never targets operations started later. */\n\trequestAbort(): HarnessAbortCapture {\n\t\tconst record = this.current;\n\t\tif (record === undefined || this.state.tag === \"idle\") {\n\t\t\treturn { signalDelivered: false };\n\t\t}\n\t\tif (this.state.tag === \"settling\") {\n\t\t\treturn { target: record.lease, signalDelivered: false };\n\t\t}\n\t\tthis.apply({ type: \"abort_request\", operationId: record.lease.operation.operationId });\n\t\tif (!record.controller.signal.aborted) record.controller.abort();\n\t\treturn { target: record.lease, signalDelivered: true };\n\t}\n\n\t/**\n\t * Sole active -> idle path. Runs `finalize` inside the settling barrier, then\n\t * releases state and resolves the lease. A finalizer failure still completes\n\t * the state release and resolves `lease.settled`, then rejects this call.\n\t */\n\tasync settle(\n\t\tlease: OperationLease,\n\t\toutcome: HarnessOperationOutcome,\n\t\tfinalize: () => Promise<void>,\n\t): Promise<HarnessOperationOutcome> {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tif (record.settleStarted) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Operation ${lease.operation.operationId} is already settling`);\n\t\t}\n\t\t// A rejected settle_begin (e.g. an attempt still open) is not a settlement.\n\t\t// Marking it as one would wedge the operation: every later settle() would\n\t\t// report \"already settling\" while the state never left `active`, and\n\t\t// `lease.settled` plus every waitForIdle() waiter would hang forever.\n\t\tthis.apply({ type: \"settle_begin\", operationId: lease.operation.operationId, outcome });\n\t\trecord.settleStarted = true;\n\t\tlet finalizeError: unknown;\n\t\ttry {\n\t\t\tawait finalize();\n\t\t} catch (error) {\n\t\t\tfinalizeError = error;\n\t\t}\n\t\tthis.apply({ type: \"settle_finish\", operationId: lease.operation.operationId });\n\t\tthis.current = undefined;\n\t\trecord.resolveSettled(outcome);\n\t\tfor (const release of [...this.idleWaiters]) {\n\t\t\tthis.idleWaiters.delete(release);\n\t\t\trelease();\n\t\t}\n\t\tif (finalizeError !== undefined) throw finalizeError;\n\t\treturn outcome;\n\t}\n\n\t/** Bounded attempt summaries for the current operation; no transcripts. */\n\tgetAttemptSummaries(lease: OperationLease): readonly HarnessAttemptSummary[] {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tconst attempts = this.state.tag === \"idle\" ? [] : this.state.attempts;\n\t\treturn attempts.map(({ attempt, outcome }) => ({\n\t\t\tattemptId: attempt.attemptId,\n\t\t\tindex: attempt.index,\n\t\t\treason: attempt.reason,\n\t\t\toutcome,\n\t\t\tstartedAtMs: attempt.startedAtMs,\n\t\t\tfinishedAtMs: record.attemptFinishTimes.get(attempt.attemptId) ?? attempt.startedAtMs,\n\t\t}));\n\t}\n\n\t/** The recorded summary for one attempt of the current operation. */\n\tgetAttemptSummary(lease: OperationLease, attempt: AttemptLease): HarnessAttemptSummary {\n\t\tconst attemptId = attempt.attempt.attemptId;\n\t\tconst summary = this.getAttemptSummaries(lease).find((candidate) => candidate.attemptId === attemptId);\n\t\tif (summary === undefined) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `No summary recorded for attempt ${attemptId}`);\n\t\t}\n\t\treturn summary;\n\t}\n\n\tasync waitForIdle(): Promise<void> {\n\t\tif (this.state.tag === \"idle\") return;\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tthis.idleWaiters.add(resolve);\n\t\t});\n\t}\n\n\tprivate expectActive(): Extract<HarnessLifecycleState, { tag: \"active\" }> {\n\t\tif (this.state.tag !== \"active\") {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Expected an active operation, state is ${this.state.tag}`);\n\t\t}\n\t\treturn this.state;\n\t}\n\n\tprivate expectCurrentLease(lease: OperationLease): OperationRecord {\n\t\tconst record = this.current;\n\t\tif (record === undefined || record.lease !== lease) {\n\t\t\tthrow new AgentHarnessError(\n\t\t\t\t\"invalid_state\",\n\t\t\t\t`Stale operation lease for ${lease.operation.operationId}; the operation no longer owns lifecycle state`,\n\t\t\t);\n\t\t}\n\t\treturn record;\n\t}\n\n\tprivate apply(command: HarnessLifecycleCommand): void {\n\t\tconst next = reduceHarnessLifecycle(this.state, command);\n\t\tif (!next.ok) throw this.asHarnessError(next.error);\n\t\tthis.state = freezeLifecycleState(next.value);\n\t}\n\n\tprivate asHarnessError(violation: HarnessLifecycleViolation): AgentHarnessError {\n\t\treturn new AgentHarnessError(violation.code === \"busy\" ? \"busy\" : \"invalid_state\", violation.message, violation);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure transition function for the harness operation lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Given the current state and one command, returns the next state or a
|
|
5
|
+
* classified violation. It performs no side effects: it does not read a
|
|
6
|
+
* clock, allocate identifiers, emit events, flush sessions, or touch abort
|
|
7
|
+
* signals. The controller supplies all of those around it.
|
|
8
|
+
*
|
|
9
|
+
* The transition table encodes the plan's legal moves plus one documented
|
|
10
|
+
* conservative addition: `structural_running -> committing` marks the single
|
|
11
|
+
* declared commit point of a structural operation. Every other transition not
|
|
12
|
+
* listed here is rejected, and rejection never mutates the input state.
|
|
13
|
+
*/
|
|
14
|
+
import { type HarnessLifecycleCommand, type HarnessLifecycleResult, type HarnessLifecycleState } from "./operation-lifecycle-types.ts";
|
|
15
|
+
type Result = HarnessLifecycleResult<HarnessLifecycleState>;
|
|
16
|
+
export declare function initialHarnessLifecycleState(): HarnessLifecycleState;
|
|
17
|
+
export declare function reduceHarnessLifecycle(state: HarnessLifecycleState, command: HarnessLifecycleCommand): Result;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=operation-lifecycle-reducer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-lifecycle-reducer.d.ts","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-reducer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAK1B,MAAM,gCAAgC,CAAC;AAIxC,KAAK,MAAM,GAAG,sBAAsB,CAAC,qBAAqB,CAAC,CAAC;AAE5D,wBAAgB,4BAA4B,IAAI,qBAAqB,CAEpE;AAkOD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,qBAAqB,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAqB7G","sourcesContent":["/**\n * Pure transition function for the harness operation lifecycle.\n *\n * Given the current state and one command, returns the next state or a\n * classified violation. It performs no side effects: it does not read a\n * clock, allocate identifiers, emit events, flush sessions, or touch abort\n * signals. The controller supplies all of those around it.\n *\n * The transition table encodes the plan's legal moves plus one documented\n * conservative addition: `structural_running -> committing` marks the single\n * declared commit point of a structural operation. Every other transition not\n * listed here is rejected, and rejection never mutates the input state.\n */\n\nimport {\n\ttype HarnessLifecycleCommand,\n\ttype HarnessLifecycleResult,\n\ttype HarnessLifecycleState,\n\tHarnessLifecycleViolation,\n\ttype HarnessLifecycleViolationCode,\n\ttype HarnessOperationKind,\n\tPROMPT_FAMILY_KINDS,\n} from \"./operation-lifecycle-types.ts\";\n\ntype ActiveState = Extract<HarnessLifecycleState, { tag: \"active\" }>;\ntype OwnedState = Extract<HarnessLifecycleState, { tag: \"active\" | \"settling\" }>;\ntype Result = HarnessLifecycleResult<HarnessLifecycleState>;\n\nexport function initialHarnessLifecycleState(): HarnessLifecycleState {\n\treturn { tag: \"idle\", lastSequence: 0 };\n}\n\nfunction reject(\n\tcode: HarnessLifecycleViolationCode,\n\tmessage: string,\n\tstate: HarnessLifecycleState,\n\tcommand: HarnessLifecycleCommand,\n): HarnessLifecycleResult<never> {\n\treturn { ok: false, error: new HarnessLifecycleViolation(code, message, state, command) };\n}\n\nfunction isPromptFamily(kind: HarnessOperationKind): boolean {\n\treturn PROMPT_FAMILY_KINDS.includes(kind);\n}\n\nfunction notIdle(state: HarnessLifecycleState, command: HarnessLifecycleCommand, action: string): Result {\n\treturn reject(\"invalid_transition\", `Cannot ${action} without an active operation`, state, command);\n}\n\n/** Commands carrying an operationId must name the current operation. */\nfunction expectCurrentOperation(\n\tstate: OwnedState,\n\toperationId: string,\n\tcommand: HarnessLifecycleCommand,\n): HarnessLifecycleResult<OwnedState> {\n\tif (state.operation.operationId !== operationId) {\n\t\treturn reject(\n\t\t\t\"stale_operation\",\n\t\t\t`Command targets operation ${operationId} but current operation is ${state.operation.operationId}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: state };\n}\n\nfunction reduceBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"begin\" }>,\n): Result {\n\tif (state.tag !== \"idle\") {\n\t\treturn reject(\"busy\", \"Cannot begin a new operation while another is active or settling\", state, command);\n\t}\n\tif (command.operation.sequence !== state.lastSequence + 1) {\n\t\treturn reject(\n\t\t\t\"sequence_violation\",\n\t\t\t`Operation sequence ${command.operation.sequence} is not lastSequence + 1 (${state.lastSequence + 1})`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn {\n\t\tok: true,\n\t\tvalue: { tag: \"active\", operation: command.operation, stage: \"preparing\", attempts: [], abortRequested: false },\n\t};\n}\n\n/**\n * Overflow recovery answers an observed overflow. It is legal only when the\n * most recently closed attempt ended with `overflow`; otherwise the operation\n * would sit in a stage from which no attempt can begin (index 0 must start\n * from `preparing`, index > 0 needs a prior overflow) and could only settle.\n */\nfunction lastAttemptOverflowed(state: ActiveState): boolean {\n\treturn state.attempts.at(-1)?.outcome === \"overflow\";\n}\n\nfunction isLegalStageMove(state: ActiveState, to: ActiveState[\"stage\"]): boolean {\n\tconst from = state.stage;\n\tconst kind = state.operation.kind;\n\tif (from === \"attempt_running\" && to === \"save_point\") return state.attempt !== undefined;\n\tif (from === \"save_point\" && to === \"attempt_running\") return state.attempt !== undefined;\n\tif (from === \"preparing\" && to === \"recovering_overflow\") {\n\t\treturn isPromptFamily(kind) && state.attempt === undefined && lastAttemptOverflowed(state);\n\t}\n\tif (from === \"preparing\" && to === \"structural_running\") return !isPromptFamily(kind);\n\treturn from === \"structural_running\" && to === \"committing\";\n}\n\nfunction reduceStage(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"stage\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"change stage\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot change stage while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\tif (!isLegalStageMove(state, command.stage)) {\n\t\treturn reject(\n\t\t\t\"invalid_transition\",\n\t\t\t`Illegal stage transition ${state.stage} -> ${command.stage} for ${state.operation.kind}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: { ...state, stage: command.stage } };\n}\n\nfunction reduceAttemptBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"attempt_begin\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"begin an attempt\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot begin an attempt while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.attempt.operationId, command);\n\tif (!current.ok) return current;\n\tif (state.attempt !== undefined) {\n\t\treturn reject(\"attempt_mismatch\", `Attempt ${state.attempt.attemptId} is still active`, state, command);\n\t}\n\tif (!isPromptFamily(state.operation.kind)) {\n\t\treturn reject(\"invalid_transition\", `${state.operation.kind} operations do not run attempts`, state, command);\n\t}\n\tif (command.attempt.index !== state.attempts.length) {\n\t\treturn reject(\n\t\t\t\"sequence_violation\",\n\t\t\t`Attempt index ${command.attempt.index} is not the next index ${state.attempts.length}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\tconst fromPreparing =\n\t\tstate.stage === \"preparing\" && command.attempt.index === 0 && command.attempt.reason === \"initial\";\n\tconst fromRecovery =\n\t\tstate.stage === \"recovering_overflow\" &&\n\t\tcommand.attempt.index > 0 &&\n\t\tcommand.attempt.reason === \"context_overflow_recovery\";\n\tif (!fromPreparing && !fromRecovery) {\n\t\treturn reject(\n\t\t\t\"invalid_transition\",\n\t\t\t`Cannot begin attempt ${command.attempt.attemptId} (${command.attempt.reason}) from stage ${state.stage}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: { ...state, stage: \"attempt_running\", attempt: command.attempt } };\n}\n\nfunction reduceAttemptEnd(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"attempt_end\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"end an attempt\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot end an attempt while settling\", state, command);\n\tif (state.attempt === undefined) return reject(\"attempt_mismatch\", \"No active attempt to end\", state, command);\n\tif (state.attempt.attemptId !== command.attemptId) {\n\t\treturn reject(\n\t\t\t\"attempt_mismatch\",\n\t\t\t`Attempt end names ${command.attemptId} but active attempt is ${state.attempt.attemptId}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\t// save_point is an intra-attempt stage between provider turns; the attempt\n\t// remains logically running there, so it may end from either stage.\n\tif (state.stage !== \"attempt_running\" && state.stage !== \"save_point\") {\n\t\treturn reject(\"invalid_transition\", `Cannot end an attempt from stage ${state.stage}`, state, command);\n\t}\n\tconst { attempt } = state;\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\t...state,\n\t\t\tstage: \"preparing\",\n\t\t\tattempt: undefined,\n\t\t\tattempts: [...state.attempts, { attempt, outcome: command.outcome }],\n\t\t},\n\t};\n}\n\nfunction reduceAbortRequest(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"abort_request\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"abort\");\n\tif (state.tag === \"settling\") return reject(\"invalid_transition\", \"Cannot abort while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\treturn { ok: true, value: { ...state, abortRequested: true } };\n}\n\nfunction reduceSettleBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"settle_begin\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"settle\");\n\tif (state.tag === \"settling\") return reject(\"invalid_transition\", \"Operation is already settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\t// Last line of defence against a silently orphaned attempt: an integration\n\t// bug that drops `attempt_end` must fail loudly here, not settle an\n\t// operation whose summary is missing a started attempt.\n\tif (state.attempt !== undefined) {\n\t\treturn reject(\n\t\t\t\"attempt_mismatch\",\n\t\t\t`Cannot settle operation ${state.operation.operationId} while attempt ${state.attempt.attemptId} is active`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\ttag: \"settling\",\n\t\t\toperation: state.operation,\n\t\t\toutcome: command.outcome,\n\t\t\tattempts: state.attempts,\n\t\t\tabortRequested: state.abortRequested,\n\t\t},\n\t};\n}\n\nfunction reduceSettleFinish(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"settle_finish\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"finish settling\");\n\tif (state.tag === \"active\")\n\t\treturn reject(\"invalid_transition\", \"Operation must pass through settling before finishing\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\treturn { ok: true, value: { tag: \"idle\", lastSequence: state.operation.sequence } };\n}\n\nexport function reduceHarnessLifecycle(state: HarnessLifecycleState, command: HarnessLifecycleCommand): Result {\n\tswitch (command.type) {\n\t\tcase \"begin\":\n\t\t\treturn reduceBegin(state, command);\n\t\tcase \"stage\":\n\t\t\treturn reduceStage(state, command);\n\t\tcase \"attempt_begin\":\n\t\t\treturn reduceAttemptBegin(state, command);\n\t\tcase \"attempt_end\":\n\t\t\treturn reduceAttemptEnd(state, command);\n\t\tcase \"abort_request\":\n\t\t\treturn reduceAbortRequest(state, command);\n\t\tcase \"settle_begin\":\n\t\t\treturn reduceSettleBegin(state, command);\n\t\tcase \"settle_finish\":\n\t\t\treturn reduceSettleFinish(state, command);\n\t\tdefault: {\n\t\t\tconst unknownCommand: never = command;\n\t\t\tthrow new HarnessLifecycleViolation(\"invalid_transition\", \"Unknown lifecycle command\", state, unknownCommand);\n\t\t}\n\t}\n}\n"]}
|