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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure outcome classification and error aggregation for harness operations.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a total function over already-observed results: it never
|
|
5
|
+
* touches lifecycle state, sessions, providers, or clocks. Keeping the rules in
|
|
6
|
+
* one leaf module makes the precedence auditable in isolation and keeps
|
|
7
|
+
* `agent-harness.ts` free of the branch-heavy classification tables.
|
|
8
|
+
*/
|
|
9
|
+
import { isContextOverflow } from "omk-ai";
|
|
10
|
+
import { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from "./types.js";
|
|
11
|
+
/**
|
|
12
|
+
* True only for an error that *is* an abort, not merely an error raised while
|
|
13
|
+
* an abort signal happened to be up. `AgentHarnessError` has no "aborted" code,
|
|
14
|
+
* so an explicit abort reaches us as a subsystem error carrying code "aborted"
|
|
15
|
+
* or as a DOM-style `AbortError`.
|
|
16
|
+
*/
|
|
17
|
+
export function isExplicitAbortError(error) {
|
|
18
|
+
const cause = toError(error);
|
|
19
|
+
if (cause.name === "AbortError")
|
|
20
|
+
return true;
|
|
21
|
+
return (cause instanceof CompactionError || cause instanceof BranchSummaryError) && cause.code === "aborted";
|
|
22
|
+
}
|
|
23
|
+
/** Map a subsystem failure onto the harness' stable top-level classification. */
|
|
24
|
+
export function normalizeHarnessError(error, fallbackCode) {
|
|
25
|
+
if (error instanceof AgentHarnessError)
|
|
26
|
+
return error;
|
|
27
|
+
const cause = toError(error);
|
|
28
|
+
if (cause instanceof SessionError)
|
|
29
|
+
return new AgentHarnessError("session", cause.message, cause);
|
|
30
|
+
if (cause instanceof CompactionError)
|
|
31
|
+
return new AgentHarnessError("compaction", cause.message, cause);
|
|
32
|
+
if (cause instanceof BranchSummaryError)
|
|
33
|
+
return new AgentHarnessError("branch_summary", cause.message, cause);
|
|
34
|
+
return new AgentHarnessError(fallbackCode, cause.message, cause);
|
|
35
|
+
}
|
|
36
|
+
/** Result-based outcome for prompt-family operations that resolve with a failure/abort assistant message. */
|
|
37
|
+
export function classifyAssistantOutcome(message) {
|
|
38
|
+
if (message.stopReason === "aborted")
|
|
39
|
+
return { status: "aborted" };
|
|
40
|
+
if (message.stopReason === "error") {
|
|
41
|
+
return { status: "failed", code: "provider", message: message.errorMessage ?? "Provider error" };
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Structural cancellation is a distinct, non-failure terminal outcome. */
|
|
46
|
+
export function classifyNavigateTreeOutcome(result) {
|
|
47
|
+
return result.cancelled ? { status: "cancelled", reason: "tree_navigation_cancelled" } : { status: "completed" };
|
|
48
|
+
}
|
|
49
|
+
/** A thrown attempt body is an aborted attempt only when the error itself is an abort. */
|
|
50
|
+
export function classifyAttemptFailure(error) {
|
|
51
|
+
return isExplicitAbortError(error) ? "aborted" : "failed";
|
|
52
|
+
}
|
|
53
|
+
/** Context overflow is a recoverable attempt outcome, not an attempt failure. */
|
|
54
|
+
export function classifyAttemptOutcome(message, contextWindow) {
|
|
55
|
+
if (message.stopReason === "aborted")
|
|
56
|
+
return "aborted";
|
|
57
|
+
if (isContextOverflow(message, contextWindow))
|
|
58
|
+
return "overflow";
|
|
59
|
+
if (message.stopReason === "error")
|
|
60
|
+
return "failed";
|
|
61
|
+
return "completed";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Single outcome-precedence rule for every public operation:
|
|
65
|
+
*
|
|
66
|
+
* session persistence failure > non-abort body/hook failure >
|
|
67
|
+
* explicit abort > result-classified outcome > completed
|
|
68
|
+
*
|
|
69
|
+
* A raised abort signal alone never downgrades another failure to "aborted":
|
|
70
|
+
* only an error that *is* an abort does. Otherwise a flush failure during an
|
|
71
|
+
* aborted turn would settle as "aborted" while the public promise rejected
|
|
72
|
+
* with "session".
|
|
73
|
+
*/
|
|
74
|
+
export function resolveOperationOutcome(input) {
|
|
75
|
+
if (input.flushError !== undefined) {
|
|
76
|
+
// Mirror `resolveOperationFailure`: a flush error that already carries a
|
|
77
|
+
// harness classification (e.g. an `invalid_state` coordinator reentry)
|
|
78
|
+
// keeps it, so the recorded outcome and the rejection never disagree.
|
|
79
|
+
const error = normalizeHarnessError(input.flushError, "session");
|
|
80
|
+
return { status: "failed", code: error.code, message: error.message };
|
|
81
|
+
}
|
|
82
|
+
if (input.bodyError !== undefined) {
|
|
83
|
+
if (isExplicitAbortError(input.bodyError))
|
|
84
|
+
return { status: "aborted" };
|
|
85
|
+
const error = normalizeHarnessError(input.bodyError, input.fallbackCode);
|
|
86
|
+
return { status: "failed", code: error.code, message: error.message };
|
|
87
|
+
}
|
|
88
|
+
return (input.classifyResult?.(input.result) ??
|
|
89
|
+
(input.signalAborted ? { status: "aborted" } : { status: "completed" }));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The error a boundary should throw after several steps may have failed, or
|
|
93
|
+
* `undefined` when none did. A single failure is returned untouched so its
|
|
94
|
+
* own classification survives; several are kept reachable through one
|
|
95
|
+
* `AggregateError`, classified by the first (primary) failure. This is what
|
|
96
|
+
* lets a failing boundary flush report *alongside* the body or listener error
|
|
97
|
+
* it followed instead of erasing it.
|
|
98
|
+
*/
|
|
99
|
+
export function combineBoundaryErrors(errors, message, fallbackCode) {
|
|
100
|
+
const present = errors.filter((error) => error !== undefined);
|
|
101
|
+
if (present.length <= 1)
|
|
102
|
+
return present[0];
|
|
103
|
+
const cause = new AggregateError(present.map(toError), message);
|
|
104
|
+
return new AgentHarnessError(normalizeHarnessError(present[0], fallbackCode).code, cause.message, cause);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Which error a public operation rejects with, or `undefined` on success.
|
|
108
|
+
*
|
|
109
|
+
* Mirrors the outcome precedence, but every concurrent cause is preserved in an
|
|
110
|
+
* `AggregateError` so an audit can still see that, say, the body and the final
|
|
111
|
+
* flush failed together.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveOperationFailure(input) {
|
|
114
|
+
const primaryError = input.bodyError ?? input.flushError;
|
|
115
|
+
if (primaryError !== undefined && input.settleError !== undefined) {
|
|
116
|
+
const cause = new AggregateError([toError(primaryError), toError(input.settleError)], "Operation failed and settlement failed");
|
|
117
|
+
return new AgentHarnessError(normalizeHarnessError(primaryError, input.fallbackCode).code, cause.message, cause);
|
|
118
|
+
}
|
|
119
|
+
if (input.settleError !== undefined)
|
|
120
|
+
return normalizeHarnessError(input.settleError, "hook");
|
|
121
|
+
if (input.flushError !== undefined) {
|
|
122
|
+
if (input.bodyError === undefined)
|
|
123
|
+
return normalizeHarnessError(input.flushError, "session");
|
|
124
|
+
const cause = new AggregateError([toError(input.bodyError), toError(input.flushError)], "Operation failed and the final flush failed");
|
|
125
|
+
return new AgentHarnessError("session", cause.message, cause);
|
|
126
|
+
}
|
|
127
|
+
if (input.bodyError !== undefined)
|
|
128
|
+
return normalizeHarnessError(input.bodyError, input.fallbackCode);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=operation-outcome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-outcome.js","sourceRoot":"","sources":["../../src/harness/operation-outcome.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure outcome classification and error aggregation for harness operations.\n *\n * Everything here is a total function over already-observed results: it never\n * touches lifecycle state, sessions, providers, or clocks. Keeping the rules in\n * one leaf module makes the precedence auditable in isolation and keeps\n * `agent-harness.ts` free of the branch-heavy classification tables.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"omk-ai\";\nimport type { HarnessAttemptOutcome, HarnessOperationOutcome } from \"./operation-lifecycle-types.ts\";\nimport type { NavigateTreeResult } from \"./types.ts\";\nimport { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from \"./types.ts\";\n\n/**\n * True only for an error that *is* an abort, not merely an error raised while\n * an abort signal happened to be up. `AgentHarnessError` has no \"aborted\" code,\n * so an explicit abort reaches us as a subsystem error carrying code \"aborted\"\n * or as a DOM-style `AbortError`.\n */\nexport function isExplicitAbortError(error: unknown): boolean {\n\tconst cause = toError(error);\n\tif (cause.name === \"AbortError\") return true;\n\treturn (cause instanceof CompactionError || cause instanceof BranchSummaryError) && cause.code === \"aborted\";\n}\n\n/** Map a subsystem failure onto the harness' stable top-level classification. */\nexport function normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError[\"code\"]): AgentHarnessError {\n\tif (error instanceof AgentHarnessError) return error;\n\tconst cause = toError(error);\n\tif (cause instanceof SessionError) return new AgentHarnessError(\"session\", cause.message, cause);\n\tif (cause instanceof CompactionError) return new AgentHarnessError(\"compaction\", cause.message, cause);\n\tif (cause instanceof BranchSummaryError) return new AgentHarnessError(\"branch_summary\", cause.message, cause);\n\treturn new AgentHarnessError(fallbackCode, cause.message, cause);\n}\n\n/** Result-based outcome for prompt-family operations that resolve with a failure/abort assistant message. */\nexport function classifyAssistantOutcome(message: AssistantMessage): HarnessOperationOutcome | undefined {\n\tif (message.stopReason === \"aborted\") return { status: \"aborted\" };\n\tif (message.stopReason === \"error\") {\n\t\treturn { status: \"failed\", code: \"provider\", message: message.errorMessage ?? \"Provider error\" };\n\t}\n\treturn undefined;\n}\n\n/** Structural cancellation is a distinct, non-failure terminal outcome. */\nexport function classifyNavigateTreeOutcome(result: NavigateTreeResult): HarnessOperationOutcome {\n\treturn result.cancelled ? { status: \"cancelled\", reason: \"tree_navigation_cancelled\" } : { status: \"completed\" };\n}\n\n/** A thrown attempt body is an aborted attempt only when the error itself is an abort. */\nexport function classifyAttemptFailure(error: unknown): HarnessAttemptOutcome {\n\treturn isExplicitAbortError(error) ? \"aborted\" : \"failed\";\n}\n\n/** Context overflow is a recoverable attempt outcome, not an attempt failure. */\nexport function classifyAttemptOutcome(\n\tmessage: AssistantMessage,\n\tcontextWindow: number | undefined,\n): HarnessAttemptOutcome {\n\tif (message.stopReason === \"aborted\") return \"aborted\";\n\tif (isContextOverflow(message, contextWindow)) return \"overflow\";\n\tif (message.stopReason === \"error\") return \"failed\";\n\treturn \"completed\";\n}\n\n/**\n * Single outcome-precedence rule for every public operation:\n *\n * session persistence failure > non-abort body/hook failure >\n * explicit abort > result-classified outcome > completed\n *\n * A raised abort signal alone never downgrades another failure to \"aborted\":\n * only an error that *is* an abort does. Otherwise a flush failure during an\n * aborted turn would settle as \"aborted\" while the public promise rejected\n * with \"session\".\n */\nexport function resolveOperationOutcome<T>(input: {\n\treadonly signalAborted: boolean;\n\treadonly result: T | undefined;\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly classifyResult: ((result: T) => HarnessOperationOutcome | undefined) | undefined;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): HarnessOperationOutcome {\n\tif (input.flushError !== undefined) {\n\t\t// Mirror `resolveOperationFailure`: a flush error that already carries a\n\t\t// harness classification (e.g. an `invalid_state` coordinator reentry)\n\t\t// keeps it, so the recorded outcome and the rejection never disagree.\n\t\tconst error = normalizeHarnessError(input.flushError, \"session\");\n\t\treturn { status: \"failed\", code: error.code, message: error.message };\n\t}\n\tif (input.bodyError !== undefined) {\n\t\tif (isExplicitAbortError(input.bodyError)) return { status: \"aborted\" };\n\t\tconst error = normalizeHarnessError(input.bodyError, input.fallbackCode);\n\t\treturn { status: \"failed\", code: error.code, message: error.message };\n\t}\n\treturn (\n\t\tinput.classifyResult?.(input.result as T) ??\n\t\t(input.signalAborted ? { status: \"aborted\" } : { status: \"completed\" })\n\t);\n}\n\n/**\n * The error a boundary should throw after several steps may have failed, or\n * `undefined` when none did. A single failure is returned untouched so its\n * own classification survives; several are kept reachable through one\n * `AggregateError`, classified by the first (primary) failure. This is what\n * lets a failing boundary flush report *alongside* the body or listener error\n * it followed instead of erasing it.\n */\nexport function combineBoundaryErrors(\n\terrors: readonly unknown[],\n\tmessage: string,\n\tfallbackCode: AgentHarnessError[\"code\"],\n): unknown {\n\tconst present = errors.filter((error) => error !== undefined);\n\tif (present.length <= 1) return present[0];\n\tconst cause = new AggregateError(present.map(toError), message);\n\treturn new AgentHarnessError(normalizeHarnessError(present[0], fallbackCode).code, cause.message, cause);\n}\n\n/**\n * Which error a public operation rejects with, or `undefined` on success.\n *\n * Mirrors the outcome precedence, but every concurrent cause is preserved in an\n * `AggregateError` so an audit can still see that, say, the body and the final\n * flush failed together.\n */\nexport function resolveOperationFailure(input: {\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly settleError: unknown;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): AgentHarnessError | undefined {\n\tconst primaryError = input.bodyError ?? input.flushError;\n\tif (primaryError !== undefined && input.settleError !== undefined) {\n\t\tconst cause = new AggregateError(\n\t\t\t[toError(primaryError), toError(input.settleError)],\n\t\t\t\"Operation failed and settlement failed\",\n\t\t);\n\t\treturn new AgentHarnessError(normalizeHarnessError(primaryError, input.fallbackCode).code, cause.message, cause);\n\t}\n\tif (input.settleError !== undefined) return normalizeHarnessError(input.settleError, \"hook\");\n\tif (input.flushError !== undefined) {\n\t\tif (input.bodyError === undefined) return normalizeHarnessError(input.flushError, \"session\");\n\t\tconst cause = new AggregateError(\n\t\t\t[toError(input.bodyError), toError(input.flushError)],\n\t\t\t\"Operation failed and the final flush failed\",\n\t\t);\n\t\treturn new AgentHarnessError(\"session\", cause.message, cause);\n\t}\n\tif (input.bodyError !== undefined) return normalizeHarnessError(input.bodyError, input.fallbackCode);\n\treturn undefined;\n}\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "omk-ai";
|
|
2
|
+
export type QueuedSessionWrite<TMessage> = {
|
|
3
|
+
readonly type: "message";
|
|
4
|
+
readonly message: TMessage;
|
|
5
|
+
} | {
|
|
6
|
+
readonly type: "thinking_level_change";
|
|
7
|
+
readonly thinkingLevel: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly type: "model_change";
|
|
10
|
+
readonly provider: string;
|
|
11
|
+
readonly modelId: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly type: "active_tools_change";
|
|
14
|
+
readonly activeToolNames: string[];
|
|
15
|
+
} | {
|
|
16
|
+
readonly type: "custom";
|
|
17
|
+
readonly customType: string;
|
|
18
|
+
readonly data?: unknown;
|
|
19
|
+
} | {
|
|
20
|
+
readonly type: "custom_message";
|
|
21
|
+
readonly customType: string;
|
|
22
|
+
readonly content: string | (TextContent | ImageContent)[];
|
|
23
|
+
readonly details?: unknown;
|
|
24
|
+
readonly display: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: "label";
|
|
27
|
+
readonly targetId: string;
|
|
28
|
+
readonly label: string | undefined;
|
|
29
|
+
} | {
|
|
30
|
+
readonly type: "session_info";
|
|
31
|
+
readonly name?: string;
|
|
32
|
+
} | {
|
|
33
|
+
readonly type: "leaf";
|
|
34
|
+
readonly targetId: string | null;
|
|
35
|
+
};
|
|
36
|
+
interface SessionWritePort<TMessage> {
|
|
37
|
+
getStorage(): {
|
|
38
|
+
setLeafId(targetId: string | null): Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
appendMessage(message: TMessage): Promise<unknown>;
|
|
41
|
+
appendThinkingLevelChange(thinkingLevel: string): Promise<unknown>;
|
|
42
|
+
appendModelChange(provider: string, modelId: string): Promise<unknown>;
|
|
43
|
+
appendActiveToolsChange(activeToolNames: string[]): Promise<unknown>;
|
|
44
|
+
appendCustomEntry(customType: string, data?: unknown): Promise<unknown>;
|
|
45
|
+
appendCustomMessageEntry(customType: string, content: string | (TextContent | ImageContent)[], display: boolean, details?: unknown): Promise<unknown>;
|
|
46
|
+
appendLabel(targetId: string, label: string | undefined): Promise<unknown>;
|
|
47
|
+
appendSessionName(name: string): Promise<unknown>;
|
|
48
|
+
moveTo(targetId: string | null): Promise<unknown>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Owns ordered pending writes and serializes coordinator-routed persistence.
|
|
52
|
+
*
|
|
53
|
+
* Persistence follows *acceptance* order, not the queue contents observed when
|
|
54
|
+
* a boundary finally runs. Each boundary captures a watermark at invocation
|
|
55
|
+
* time and drains only writes accepted at or before it, so a write enqueued
|
|
56
|
+
* after an idle boundary was reserved can never overtake it.
|
|
57
|
+
*/
|
|
58
|
+
export declare class SessionWriteCoordinator<TMessage> {
|
|
59
|
+
private readonly pendingWrites;
|
|
60
|
+
private nextSequence;
|
|
61
|
+
private operationTail?;
|
|
62
|
+
private invokingOperation;
|
|
63
|
+
private readonly session;
|
|
64
|
+
constructor(session: SessionWritePort<TMessage>);
|
|
65
|
+
enqueue(write: QueuedSessionWrite<TMessage>): void;
|
|
66
|
+
hasPending(): boolean;
|
|
67
|
+
snapshot(): readonly QueuedSessionWrite<TMessage>[];
|
|
68
|
+
flush(): Promise<void>;
|
|
69
|
+
persistAfterPending(write: QueuedSessionWrite<TMessage>): Promise<void>;
|
|
70
|
+
private flushPendingThrough;
|
|
71
|
+
private persistIdleWrite;
|
|
72
|
+
private persist;
|
|
73
|
+
private serialize;
|
|
74
|
+
}
|
|
75
|
+
export {};
|
|
76
|
+
//# sourceMappingURL=session-write-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-write-coordinator.d.ts","sourceRoot":"","sources":["../../src/harness/session-write-coordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent, TextContent } from \"omk-ai\";\nimport { createImmutableSnapshot } from \"../plain-data.ts\";\nimport { AgentHarnessError } from \"./errors.ts\";\n\nexport type QueuedSessionWrite<TMessage> =\n\t| { readonly type: \"message\"; readonly message: 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| { 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 SessionWritePort<TMessage> {\n\tgetStorage(): { setLeafId(targetId: string | null): Promise<void> };\n\tappendMessage(message: TMessage): Promise<unknown>;\n\tappendThinkingLevelChange(thinkingLevel: string): Promise<unknown>;\n\tappendModelChange(provider: string, modelId: string): Promise<unknown>;\n\tappendActiveToolsChange(activeToolNames: string[]): Promise<unknown>;\n\tappendCustomEntry(customType: string, data?: unknown): Promise<unknown>;\n\tappendCustomMessageEntry(\n\t\tcustomType: string,\n\t\tcontent: string | (TextContent | ImageContent)[],\n\t\tdisplay: boolean,\n\t\tdetails?: unknown,\n\t): Promise<unknown>;\n\tappendLabel(targetId: string, label: string | undefined): Promise<unknown>;\n\tappendSessionName(name: string): Promise<unknown>;\n\tmoveTo(targetId: string | null): Promise<unknown>;\n}\n\n/** A queued write tagged with the monotonic order in which it was accepted. */\ninterface SequencedSessionWrite<TMessage> {\n\treadonly sequence: number;\n\treadonly write: QueuedSessionWrite<TMessage>;\n}\n\n/**\n * Owns ordered pending writes and serializes coordinator-routed persistence.\n *\n * Persistence follows *acceptance* order, not the queue contents observed when\n * a boundary finally runs. Each boundary captures a watermark at invocation\n * time and drains only writes accepted at or before it, so a write enqueued\n * after an idle boundary was reserved can never overtake it.\n */\nexport class SessionWriteCoordinator<TMessage> {\n\tprivate readonly pendingWrites: SequencedSessionWrite<TMessage>[] = [];\n\tprivate nextSequence = 1;\n\tprivate operationTail?: Promise<void>;\n\tprivate invokingOperation = false;\n\tprivate readonly session: SessionWritePort<TMessage>;\n\n\tconstructor(session: SessionWritePort<TMessage>) {\n\t\tthis.session = session;\n\t}\n\n\tenqueue(write: QueuedSessionWrite<TMessage>): void {\n\t\tthis.pendingWrites.push({ sequence: this.nextSequence++, write: createImmutableSnapshot(write) });\n\t}\n\n\thasPending(): boolean {\n\t\treturn this.pendingWrites.length > 0;\n\t}\n\n\tsnapshot(): readonly QueuedSessionWrite<TMessage>[] {\n\t\t// Each queued write was frozen on acceptance, so exposing them needs no clone.\n\t\treturn Object.freeze(this.pendingWrites.map((entry) => entry.write));\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tconst acceptedThrough = this.nextSequence - 1;\n\t\tawait this.serialize(async () => await this.flushPendingThrough(acceptedThrough));\n\t}\n\n\tasync persistAfterPending(write: QueuedSessionWrite<TMessage>): Promise<void> {\n\t\tconst snapshot = createImmutableSnapshot(write);\n\t\t// Reserve the boundary now: only writes already accepted precede this one.\n\t\tconst acceptedThrough = this.nextSequence - 1;\n\t\tawait this.serialize(async () => {\n\t\t\tawait this.flushPendingThrough(acceptedThrough);\n\t\t\tawait this.persistIdleWrite(snapshot);\n\t\t});\n\t}\n\n\t/** Drains queued writes up to `acceptedThrough`; a failed head stays at the head. */\n\tprivate async flushPendingThrough(acceptedThrough: number): Promise<void> {\n\t\twhile (this.pendingWrites.length > 0) {\n\t\t\tconst head = this.pendingWrites[0];\n\t\t\tif (head === undefined || head.sequence > acceptedThrough) return;\n\t\t\tawait this.persist(head.write);\n\t\t\tthis.pendingWrites.shift();\n\t\t}\n\t}\n\n\t/** Idle leaf moves keep the summarizing `Session.moveTo()` path. */\n\tprivate async persistIdleWrite(write: QueuedSessionWrite<TMessage>): Promise<void> {\n\t\tif (write.type === \"leaf\") {\n\t\t\tawait this.session.moveTo(write.targetId);\n\t\t\treturn;\n\t\t}\n\t\tawait this.persist(write);\n\t}\n\n\tprivate async persist(write: QueuedSessionWrite<TMessage>): Promise<void> {\n\t\tswitch (write.type) {\n\t\t\tcase \"message\":\n\t\t\t\tawait this.session.appendMessage(write.message);\n\t\t\t\treturn;\n\t\t\tcase \"thinking_level_change\":\n\t\t\t\tawait this.session.appendThinkingLevelChange(write.thinkingLevel);\n\t\t\t\treturn;\n\t\t\tcase \"model_change\":\n\t\t\t\tawait this.session.appendModelChange(write.provider, write.modelId);\n\t\t\t\treturn;\n\t\t\tcase \"active_tools_change\":\n\t\t\t\tawait this.session.appendActiveToolsChange([...write.activeToolNames]);\n\t\t\t\treturn;\n\t\t\tcase \"custom\":\n\t\t\t\tawait this.session.appendCustomEntry(write.customType, write.data);\n\t\t\t\treturn;\n\t\t\tcase \"custom_message\":\n\t\t\t\tawait this.session.appendCustomMessageEntry(\n\t\t\t\t\twrite.customType,\n\t\t\t\t\ttypeof write.content === \"string\" ? write.content : [...write.content],\n\t\t\t\t\twrite.display,\n\t\t\t\t\twrite.details,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\tcase \"label\":\n\t\t\t\tawait this.session.appendLabel(write.targetId, write.label);\n\t\t\t\treturn;\n\t\t\tcase \"session_info\":\n\t\t\t\tawait this.session.appendSessionName(write.name ?? \"\");\n\t\t\t\treturn;\n\t\t\tcase \"leaf\":\n\t\t\t\tawait this.session.getStorage().setLeafId(write.targetId);\n\t\t\t\treturn;\n\t\t\tdefault: {\n\t\t\t\tconst unsupportedWrite: never = write;\n\t\t\t\tthrow new AgentHarnessError(\n\t\t\t\t\t\"invalid_state\",\n\t\t\t\t\t`Unsupported pending session write: ${String(unsupportedWrite)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate serialize(operation: () => Promise<void>): Promise<void> {\n\t\tif (this.invokingOperation) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", \"Session persistence cannot synchronously reenter itself\");\n\t\t}\n\t\tconst previous = this.operationTail;\n\t\tlet releaseTail = (): void => undefined;\n\t\tconst tail = new Promise<void>((resolve) => {\n\t\t\treleaseTail = resolve;\n\t\t});\n\t\tthis.operationTail = tail;\n\t\tconst invoke = (): Promise<void> => {\n\t\t\tthis.invokingOperation = true;\n\t\t\ttry {\n\t\t\t\treturn operation();\n\t\t\t} finally {\n\t\t\t\tthis.invokingOperation = false;\n\t\t\t}\n\t\t};\n\t\tconst next = previous ? previous.then(invoke) : invoke();\n\t\tconst release = (): void => {\n\t\t\treleaseTail();\n\t\t\tif (this.operationTail === tail) this.operationTail = undefined;\n\t\t};\n\t\t// Keep the serialization tail usable after a caller observes a failed write.\n\t\tvoid next.then(release, release);\n\t\treturn next;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createImmutableSnapshot } from "../plain-data.js";
|
|
2
|
+
import { AgentHarnessError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Owns ordered pending writes and serializes coordinator-routed persistence.
|
|
5
|
+
*
|
|
6
|
+
* Persistence follows *acceptance* order, not the queue contents observed when
|
|
7
|
+
* a boundary finally runs. Each boundary captures a watermark at invocation
|
|
8
|
+
* time and drains only writes accepted at or before it, so a write enqueued
|
|
9
|
+
* after an idle boundary was reserved can never overtake it.
|
|
10
|
+
*/
|
|
11
|
+
export class SessionWriteCoordinator {
|
|
12
|
+
pendingWrites = [];
|
|
13
|
+
nextSequence = 1;
|
|
14
|
+
operationTail;
|
|
15
|
+
invokingOperation = false;
|
|
16
|
+
session;
|
|
17
|
+
constructor(session) {
|
|
18
|
+
this.session = session;
|
|
19
|
+
}
|
|
20
|
+
enqueue(write) {
|
|
21
|
+
this.pendingWrites.push({ sequence: this.nextSequence++, write: createImmutableSnapshot(write) });
|
|
22
|
+
}
|
|
23
|
+
hasPending() {
|
|
24
|
+
return this.pendingWrites.length > 0;
|
|
25
|
+
}
|
|
26
|
+
snapshot() {
|
|
27
|
+
// Each queued write was frozen on acceptance, so exposing them needs no clone.
|
|
28
|
+
return Object.freeze(this.pendingWrites.map((entry) => entry.write));
|
|
29
|
+
}
|
|
30
|
+
async flush() {
|
|
31
|
+
const acceptedThrough = this.nextSequence - 1;
|
|
32
|
+
await this.serialize(async () => await this.flushPendingThrough(acceptedThrough));
|
|
33
|
+
}
|
|
34
|
+
async persistAfterPending(write) {
|
|
35
|
+
const snapshot = createImmutableSnapshot(write);
|
|
36
|
+
// Reserve the boundary now: only writes already accepted precede this one.
|
|
37
|
+
const acceptedThrough = this.nextSequence - 1;
|
|
38
|
+
await this.serialize(async () => {
|
|
39
|
+
await this.flushPendingThrough(acceptedThrough);
|
|
40
|
+
await this.persistIdleWrite(snapshot);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Drains queued writes up to `acceptedThrough`; a failed head stays at the head. */
|
|
44
|
+
async flushPendingThrough(acceptedThrough) {
|
|
45
|
+
while (this.pendingWrites.length > 0) {
|
|
46
|
+
const head = this.pendingWrites[0];
|
|
47
|
+
if (head === undefined || head.sequence > acceptedThrough)
|
|
48
|
+
return;
|
|
49
|
+
await this.persist(head.write);
|
|
50
|
+
this.pendingWrites.shift();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Idle leaf moves keep the summarizing `Session.moveTo()` path. */
|
|
54
|
+
async persistIdleWrite(write) {
|
|
55
|
+
if (write.type === "leaf") {
|
|
56
|
+
await this.session.moveTo(write.targetId);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
await this.persist(write);
|
|
60
|
+
}
|
|
61
|
+
async persist(write) {
|
|
62
|
+
switch (write.type) {
|
|
63
|
+
case "message":
|
|
64
|
+
await this.session.appendMessage(write.message);
|
|
65
|
+
return;
|
|
66
|
+
case "thinking_level_change":
|
|
67
|
+
await this.session.appendThinkingLevelChange(write.thinkingLevel);
|
|
68
|
+
return;
|
|
69
|
+
case "model_change":
|
|
70
|
+
await this.session.appendModelChange(write.provider, write.modelId);
|
|
71
|
+
return;
|
|
72
|
+
case "active_tools_change":
|
|
73
|
+
await this.session.appendActiveToolsChange([...write.activeToolNames]);
|
|
74
|
+
return;
|
|
75
|
+
case "custom":
|
|
76
|
+
await this.session.appendCustomEntry(write.customType, write.data);
|
|
77
|
+
return;
|
|
78
|
+
case "custom_message":
|
|
79
|
+
await this.session.appendCustomMessageEntry(write.customType, typeof write.content === "string" ? write.content : [...write.content], write.display, write.details);
|
|
80
|
+
return;
|
|
81
|
+
case "label":
|
|
82
|
+
await this.session.appendLabel(write.targetId, write.label);
|
|
83
|
+
return;
|
|
84
|
+
case "session_info":
|
|
85
|
+
await this.session.appendSessionName(write.name ?? "");
|
|
86
|
+
return;
|
|
87
|
+
case "leaf":
|
|
88
|
+
await this.session.getStorage().setLeafId(write.targetId);
|
|
89
|
+
return;
|
|
90
|
+
default: {
|
|
91
|
+
const unsupportedWrite = write;
|
|
92
|
+
throw new AgentHarnessError("invalid_state", `Unsupported pending session write: ${String(unsupportedWrite)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
serialize(operation) {
|
|
97
|
+
if (this.invokingOperation) {
|
|
98
|
+
throw new AgentHarnessError("invalid_state", "Session persistence cannot synchronously reenter itself");
|
|
99
|
+
}
|
|
100
|
+
const previous = this.operationTail;
|
|
101
|
+
let releaseTail = () => undefined;
|
|
102
|
+
const tail = new Promise((resolve) => {
|
|
103
|
+
releaseTail = resolve;
|
|
104
|
+
});
|
|
105
|
+
this.operationTail = tail;
|
|
106
|
+
const invoke = () => {
|
|
107
|
+
this.invokingOperation = true;
|
|
108
|
+
try {
|
|
109
|
+
return operation();
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.invokingOperation = false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const next = previous ? previous.then(invoke) : invoke();
|
|
116
|
+
const release = () => {
|
|
117
|
+
releaseTail();
|
|
118
|
+
if (this.operationTail === tail)
|
|
119
|
+
this.operationTail = undefined;
|
|
120
|
+
};
|
|
121
|
+
// Keep the serialization tail usable after a caller observes a failed write.
|
|
122
|
+
void next.then(release, release);
|
|
123
|
+
return next;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=session-write-coordinator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-write-coordinator.js","sourceRoot":"","sources":["../../src/harness/session-write-coordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent, TextContent } from \"omk-ai\";\nimport { createImmutableSnapshot } from \"../plain-data.ts\";\nimport { AgentHarnessError } from \"./errors.ts\";\n\nexport type QueuedSessionWrite<TMessage> =\n\t| { readonly type: \"message\"; readonly message: 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| { 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 SessionWritePort<TMessage> {\n\tgetStorage(): { setLeafId(targetId: string | null): Promise<void> };\n\tappendMessage(message: TMessage): Promise<unknown>;\n\tappendThinkingLevelChange(thinkingLevel: string): Promise<unknown>;\n\tappendModelChange(provider: string, modelId: string): Promise<unknown>;\n\tappendActiveToolsChange(activeToolNames: string[]): Promise<unknown>;\n\tappendCustomEntry(customType: string, data?: unknown): Promise<unknown>;\n\tappendCustomMessageEntry(\n\t\tcustomType: string,\n\t\tcontent: string | (TextContent | ImageContent)[],\n\t\tdisplay: boolean,\n\t\tdetails?: unknown,\n\t): Promise<unknown>;\n\tappendLabel(targetId: string, label: string | undefined): Promise<unknown>;\n\tappendSessionName(name: string): Promise<unknown>;\n\tmoveTo(targetId: string | null): Promise<unknown>;\n}\n\n/** A queued write tagged with the monotonic order in which it was accepted. */\ninterface SequencedSessionWrite<TMessage> {\n\treadonly sequence: number;\n\treadonly write: QueuedSessionWrite<TMessage>;\n}\n\n/**\n * Owns ordered pending writes and serializes coordinator-routed persistence.\n *\n * Persistence follows *acceptance* order, not the queue contents observed when\n * a boundary finally runs. Each boundary captures a watermark at invocation\n * time and drains only writes accepted at or before it, so a write enqueued\n * after an idle boundary was reserved can never overtake it.\n */\nexport class SessionWriteCoordinator<TMessage> {\n\tprivate readonly pendingWrites: SequencedSessionWrite<TMessage>[] = [];\n\tprivate nextSequence = 1;\n\tprivate operationTail?: Promise<void>;\n\tprivate invokingOperation = false;\n\tprivate readonly session: SessionWritePort<TMessage>;\n\n\tconstructor(session: SessionWritePort<TMessage>) {\n\t\tthis.session = session;\n\t}\n\n\tenqueue(write: QueuedSessionWrite<TMessage>): void {\n\t\tthis.pendingWrites.push({ sequence: this.nextSequence++, write: createImmutableSnapshot(write) });\n\t}\n\n\thasPending(): boolean {\n\t\treturn this.pendingWrites.length > 0;\n\t}\n\n\tsnapshot(): readonly QueuedSessionWrite<TMessage>[] {\n\t\t// Each queued write was frozen on acceptance, so exposing them needs no clone.\n\t\treturn Object.freeze(this.pendingWrites.map((entry) => entry.write));\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tconst acceptedThrough = this.nextSequence - 1;\n\t\tawait this.serialize(async () => await this.flushPendingThrough(acceptedThrough));\n\t}\n\n\tasync persistAfterPending(write: QueuedSessionWrite<TMessage>): Promise<void> {\n\t\tconst snapshot = createImmutableSnapshot(write);\n\t\t// Reserve the boundary now: only writes already accepted precede this one.\n\t\tconst acceptedThrough = this.nextSequence - 1;\n\t\tawait this.serialize(async () => {\n\t\t\tawait this.flushPendingThrough(acceptedThrough);\n\t\t\tawait this.persistIdleWrite(snapshot);\n\t\t});\n\t}\n\n\t/** Drains queued writes up to `acceptedThrough`; a failed head stays at the head. */\n\tprivate async flushPendingThrough(acceptedThrough: number): Promise<void> {\n\t\twhile (this.pendingWrites.length > 0) {\n\t\t\tconst head = this.pendingWrites[0];\n\t\t\tif (head === undefined || head.sequence > acceptedThrough) return;\n\t\t\tawait this.persist(head.write);\n\t\t\tthis.pendingWrites.shift();\n\t\t}\n\t}\n\n\t/** Idle leaf moves keep the summarizing `Session.moveTo()` path. */\n\tprivate async persistIdleWrite(write: QueuedSessionWrite<TMessage>): Promise<void> {\n\t\tif (write.type === \"leaf\") {\n\t\t\tawait this.session.moveTo(write.targetId);\n\t\t\treturn;\n\t\t}\n\t\tawait this.persist(write);\n\t}\n\n\tprivate async persist(write: QueuedSessionWrite<TMessage>): Promise<void> {\n\t\tswitch (write.type) {\n\t\t\tcase \"message\":\n\t\t\t\tawait this.session.appendMessage(write.message);\n\t\t\t\treturn;\n\t\t\tcase \"thinking_level_change\":\n\t\t\t\tawait this.session.appendThinkingLevelChange(write.thinkingLevel);\n\t\t\t\treturn;\n\t\t\tcase \"model_change\":\n\t\t\t\tawait this.session.appendModelChange(write.provider, write.modelId);\n\t\t\t\treturn;\n\t\t\tcase \"active_tools_change\":\n\t\t\t\tawait this.session.appendActiveToolsChange([...write.activeToolNames]);\n\t\t\t\treturn;\n\t\t\tcase \"custom\":\n\t\t\t\tawait this.session.appendCustomEntry(write.customType, write.data);\n\t\t\t\treturn;\n\t\t\tcase \"custom_message\":\n\t\t\t\tawait this.session.appendCustomMessageEntry(\n\t\t\t\t\twrite.customType,\n\t\t\t\t\ttypeof write.content === \"string\" ? write.content : [...write.content],\n\t\t\t\t\twrite.display,\n\t\t\t\t\twrite.details,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\tcase \"label\":\n\t\t\t\tawait this.session.appendLabel(write.targetId, write.label);\n\t\t\t\treturn;\n\t\t\tcase \"session_info\":\n\t\t\t\tawait this.session.appendSessionName(write.name ?? \"\");\n\t\t\t\treturn;\n\t\t\tcase \"leaf\":\n\t\t\t\tawait this.session.getStorage().setLeafId(write.targetId);\n\t\t\t\treturn;\n\t\t\tdefault: {\n\t\t\t\tconst unsupportedWrite: never = write;\n\t\t\t\tthrow new AgentHarnessError(\n\t\t\t\t\t\"invalid_state\",\n\t\t\t\t\t`Unsupported pending session write: ${String(unsupportedWrite)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate serialize(operation: () => Promise<void>): Promise<void> {\n\t\tif (this.invokingOperation) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", \"Session persistence cannot synchronously reenter itself\");\n\t\t}\n\t\tconst previous = this.operationTail;\n\t\tlet releaseTail = (): void => undefined;\n\t\tconst tail = new Promise<void>((resolve) => {\n\t\t\treleaseTail = resolve;\n\t\t});\n\t\tthis.operationTail = tail;\n\t\tconst invoke = (): Promise<void> => {\n\t\t\tthis.invokingOperation = true;\n\t\t\ttry {\n\t\t\t\treturn operation();\n\t\t\t} finally {\n\t\t\t\tthis.invokingOperation = false;\n\t\t\t}\n\t\t};\n\t\tconst next = previous ? previous.then(invoke) : invoke();\n\t\tconst release = (): void => {\n\t\t\treleaseTail();\n\t\t\tif (this.operationTail === tail) this.operationTail = undefined;\n\t\t};\n\t\t// Keep the serialization tail usable after a caller observes a failed write.\n\t\tvoid next.then(release, release);\n\t\treturn next;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscriber fan-out with a self-wait barrier, extracted from `AgentHarness`.
|
|
3
|
+
*
|
|
4
|
+
* A listener that awaits the end of the very operation whose emission it is
|
|
5
|
+
* blocking forms a cycle. The call that closes that cycle (`waitForIdle()`,
|
|
6
|
+
* `abort()`) is always made during the listener's *synchronous* prologue — an
|
|
7
|
+
* async function body runs synchronously up to its first `await` — so marking
|
|
8
|
+
* just that window identifies callback-originated waits exactly, without
|
|
9
|
+
* falsely rejecting an unrelated concurrent caller that runs later while the
|
|
10
|
+
* listener's promise is merely pending.
|
|
11
|
+
*
|
|
12
|
+
* This module imports no harness or session types so it stays a leaf for the
|
|
13
|
+
* import-cycle ratchet; the harness passes the current operation id in.
|
|
14
|
+
*/
|
|
15
|
+
export type SubscriberListener<TEvent> = (event: TEvent, signal?: AbortSignal) => Promise<void> | void;
|
|
16
|
+
export declare class SubscriberFanout<TEvent extends {
|
|
17
|
+
readonly type: string;
|
|
18
|
+
}> {
|
|
19
|
+
private readonly listeners;
|
|
20
|
+
/** Non-zero only while a subscriber's synchronous prologue is on the stack. */
|
|
21
|
+
private syncFrameDepth;
|
|
22
|
+
private operationId;
|
|
23
|
+
private eventType;
|
|
24
|
+
subscribe(listener: SubscriberListener<TEvent>): () => void;
|
|
25
|
+
/** Deliver `event` to every subscriber in order; a throwing subscriber fails the emission as `hook`. */
|
|
26
|
+
emit(event: TEvent, currentOperationId: string | undefined, signal?: AbortSignal): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Fail closed when an awaited listener tries to wait on its own operation.
|
|
29
|
+
* Rejecting immediately turns a permanent deadlock into a classified error.
|
|
30
|
+
*
|
|
31
|
+
* Scope: this catches a wait issued from the listener's synchronous prologue,
|
|
32
|
+
* which covers `await harness.waitForIdle()` / `await harness.abort()`. A wait
|
|
33
|
+
* deferred behind an unrelated `await` inside the listener is not detected.
|
|
34
|
+
*/
|
|
35
|
+
assertNotSelfWait(api: string, currentOperationId: string | undefined): void;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=subscriber-fanout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriber-fanout.d.ts","sourceRoot":"","sources":["../../src/harness/subscriber-fanout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,MAAM,MAAM,kBAAkB,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEvG,qBAAa,gBAAgB,CAAC,MAAM,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IACnE,+EAA+E;IAC/E,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,SAAS,CAAqB;IAEtC,SAAS,CAAC,QAAQ,EAAE,kBAAkB,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAG1D;IAED,wGAAwG;IAClG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BrG;IAED;;;;;;;OAOG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAO3E;CACD","sourcesContent":["/**\n * Subscriber fan-out with a self-wait barrier, extracted from `AgentHarness`.\n *\n * A listener that awaits the end of the very operation whose emission it is\n * blocking forms a cycle. The call that closes that cycle (`waitForIdle()`,\n * `abort()`) is always made during the listener's *synchronous* prologue — an\n * async function body runs synchronously up to its first `await` — so marking\n * just that window identifies callback-originated waits exactly, without\n * falsely rejecting an unrelated concurrent caller that runs later while the\n * listener's promise is merely pending.\n *\n * This module imports no harness or session types so it stays a leaf for the\n * import-cycle ratchet; the harness passes the current operation id in.\n */\n\nimport { AgentHarnessError } from \"./errors.ts\";\nimport { normalizeHarnessError } from \"./operation-outcome.ts\";\n\nexport type SubscriberListener<TEvent> = (event: TEvent, signal?: AbortSignal) => Promise<void> | void;\n\nexport class SubscriberFanout<TEvent extends { readonly type: string }> {\n\tprivate readonly listeners = new Set<SubscriberListener<TEvent>>();\n\t/** Non-zero only while a subscriber's synchronous prologue is on the stack. */\n\tprivate syncFrameDepth = 0;\n\tprivate operationId: string | undefined;\n\tprivate eventType: string | undefined;\n\n\tsubscribe(listener: SubscriberListener<TEvent>): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => this.listeners.delete(listener);\n\t}\n\n\t/** Deliver `event` to every subscriber in order; a throwing subscriber fails the emission as `hook`. */\n\tasync emit(event: TEvent, currentOperationId: string | undefined, signal?: AbortSignal): Promise<void> {\n\t\tfor (const listener of this.listeners) {\n\t\t\tconst previousOperationId = this.operationId;\n\t\t\tconst previousEventType = this.eventType;\n\t\t\tthis.operationId = currentOperationId;\n\t\t\tthis.eventType = event.type;\n\t\t\tlet pending: Promise<unknown> | unknown;\n\t\t\tthis.syncFrameDepth += 1;\n\t\t\ttry {\n\t\t\t\tpending = listener(event, signal);\n\t\t\t} catch (error) {\n\t\t\t\tthis.operationId = previousOperationId;\n\t\t\t\tthis.eventType = previousEventType;\n\t\t\t\tthrow normalizeHarnessError(error, \"hook\");\n\t\t\t} finally {\n\t\t\t\tthis.syncFrameDepth -= 1;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait pending;\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHarnessError(error, \"hook\");\n\t\t\t} finally {\n\t\t\t\tthis.operationId = previousOperationId;\n\t\t\t\tthis.eventType = previousEventType;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Fail closed when an awaited listener tries to wait on its own operation.\n\t * Rejecting immediately turns a permanent deadlock into a classified error.\n\t *\n\t * Scope: this catches a wait issued from the listener's synchronous prologue,\n\t * which covers `await harness.waitForIdle()` / `await harness.abort()`. A wait\n\t * deferred behind an unrelated `await` inside the listener is not detected.\n\t */\n\tassertNotSelfWait(api: string, currentOperationId: string | undefined): void {\n\t\tif (this.syncFrameDepth > 0 && currentOperationId !== undefined && currentOperationId === this.operationId) {\n\t\t\tthrow new AgentHarnessError(\n\t\t\t\t\"invalid_state\",\n\t\t\t\t`${api} cannot await the current operation from an awaited ${this.eventType ?? \"unknown\"} callback`,\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscriber fan-out with a self-wait barrier, extracted from `AgentHarness`.
|
|
3
|
+
*
|
|
4
|
+
* A listener that awaits the end of the very operation whose emission it is
|
|
5
|
+
* blocking forms a cycle. The call that closes that cycle (`waitForIdle()`,
|
|
6
|
+
* `abort()`) is always made during the listener's *synchronous* prologue — an
|
|
7
|
+
* async function body runs synchronously up to its first `await` — so marking
|
|
8
|
+
* just that window identifies callback-originated waits exactly, without
|
|
9
|
+
* falsely rejecting an unrelated concurrent caller that runs later while the
|
|
10
|
+
* listener's promise is merely pending.
|
|
11
|
+
*
|
|
12
|
+
* This module imports no harness or session types so it stays a leaf for the
|
|
13
|
+
* import-cycle ratchet; the harness passes the current operation id in.
|
|
14
|
+
*/
|
|
15
|
+
import { AgentHarnessError } from "./errors.js";
|
|
16
|
+
import { normalizeHarnessError } from "./operation-outcome.js";
|
|
17
|
+
export class SubscriberFanout {
|
|
18
|
+
listeners = new Set();
|
|
19
|
+
/** Non-zero only while a subscriber's synchronous prologue is on the stack. */
|
|
20
|
+
syncFrameDepth = 0;
|
|
21
|
+
operationId;
|
|
22
|
+
eventType;
|
|
23
|
+
subscribe(listener) {
|
|
24
|
+
this.listeners.add(listener);
|
|
25
|
+
return () => this.listeners.delete(listener);
|
|
26
|
+
}
|
|
27
|
+
/** Deliver `event` to every subscriber in order; a throwing subscriber fails the emission as `hook`. */
|
|
28
|
+
async emit(event, currentOperationId, signal) {
|
|
29
|
+
for (const listener of this.listeners) {
|
|
30
|
+
const previousOperationId = this.operationId;
|
|
31
|
+
const previousEventType = this.eventType;
|
|
32
|
+
this.operationId = currentOperationId;
|
|
33
|
+
this.eventType = event.type;
|
|
34
|
+
let pending;
|
|
35
|
+
this.syncFrameDepth += 1;
|
|
36
|
+
try {
|
|
37
|
+
pending = listener(event, signal);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
this.operationId = previousOperationId;
|
|
41
|
+
this.eventType = previousEventType;
|
|
42
|
+
throw normalizeHarnessError(error, "hook");
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
this.syncFrameDepth -= 1;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await pending;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw normalizeHarnessError(error, "hook");
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
this.operationId = previousOperationId;
|
|
55
|
+
this.eventType = previousEventType;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Fail closed when an awaited listener tries to wait on its own operation.
|
|
61
|
+
* Rejecting immediately turns a permanent deadlock into a classified error.
|
|
62
|
+
*
|
|
63
|
+
* Scope: this catches a wait issued from the listener's synchronous prologue,
|
|
64
|
+
* which covers `await harness.waitForIdle()` / `await harness.abort()`. A wait
|
|
65
|
+
* deferred behind an unrelated `await` inside the listener is not detected.
|
|
66
|
+
*/
|
|
67
|
+
assertNotSelfWait(api, currentOperationId) {
|
|
68
|
+
if (this.syncFrameDepth > 0 && currentOperationId !== undefined && currentOperationId === this.operationId) {
|
|
69
|
+
throw new AgentHarnessError("invalid_state", `${api} cannot await the current operation from an awaited ${this.eventType ?? "unknown"} callback`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=subscriber-fanout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriber-fanout.js","sourceRoot":"","sources":["../../src/harness/subscriber-fanout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAI/D,MAAM,OAAO,gBAAgB;IACX,SAAS,GAAG,IAAI,GAAG,EAA8B,CAAC;IACnE,+EAA+E;IACvE,cAAc,GAAG,CAAC,CAAC;IACnB,WAAW,CAAqB;IAChC,SAAS,CAAqB;IAEtC,SAAS,CAAC,QAAoC,EAAc;QAC3D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAAA,CAC7C;IAED,wGAAwG;IACxG,KAAK,CAAC,IAAI,CAAC,KAAa,EAAE,kBAAsC,EAAE,MAAoB,EAAiB;QACtG,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACvC,MAAM,mBAAmB,GAAG,IAAI,CAAC,WAAW,CAAC;YAC7C,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC;YACzC,IAAI,CAAC,WAAW,GAAG,kBAAkB,CAAC;YACtC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC;YAC5B,IAAI,OAAmC,CAAC;YACxC,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC;YACzB,IAAI,CAAC;gBACJ,OAAO,GAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACnC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,CAAC,WAAW,GAAG,mBAAmB,CAAC;gBACvC,IAAI,CAAC,SAAS,GAAG,iBAAiB,CAAC;gBACnC,MAAM,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC5C,CAAC;oBAAS,CAAC;gBACV,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC;YAC1B,CAAC;YACD,IAAI,CAAC;gBACJ,MAAM,OAAO,CAAC;YACf,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC5C,CAAC;oBAAS,CAAC;gBACV,IAAI,CAAC,WAAW,GAAG,mBAAmB,CAAC;gBACvC,IAAI,CAAC,SAAS,GAAG,iBAAiB,CAAC;YACpC,CAAC;QACF,CAAC;IAAA,CACD;IAED;;;;;;;OAOG;IACH,iBAAiB,CAAC,GAAW,EAAE,kBAAsC,EAAQ;QAC5E,IAAI,IAAI,CAAC,cAAc,GAAG,CAAC,IAAI,kBAAkB,KAAK,SAAS,IAAI,kBAAkB,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;YAC5G,MAAM,IAAI,iBAAiB,CAC1B,eAAe,EACf,GAAG,GAAG,uDAAuD,IAAI,CAAC,SAAS,IAAI,SAAS,WAAW,CACnG,CAAC;QACH,CAAC;IAAA,CACD;CACD","sourcesContent":["/**\n * Subscriber fan-out with a self-wait barrier, extracted from `AgentHarness`.\n *\n * A listener that awaits the end of the very operation whose emission it is\n * blocking forms a cycle. The call that closes that cycle (`waitForIdle()`,\n * `abort()`) is always made during the listener's *synchronous* prologue — an\n * async function body runs synchronously up to its first `await` — so marking\n * just that window identifies callback-originated waits exactly, without\n * falsely rejecting an unrelated concurrent caller that runs later while the\n * listener's promise is merely pending.\n *\n * This module imports no harness or session types so it stays a leaf for the\n * import-cycle ratchet; the harness passes the current operation id in.\n */\n\nimport { AgentHarnessError } from \"./errors.ts\";\nimport { normalizeHarnessError } from \"./operation-outcome.ts\";\n\nexport type SubscriberListener<TEvent> = (event: TEvent, signal?: AbortSignal) => Promise<void> | void;\n\nexport class SubscriberFanout<TEvent extends { readonly type: string }> {\n\tprivate readonly listeners = new Set<SubscriberListener<TEvent>>();\n\t/** Non-zero only while a subscriber's synchronous prologue is on the stack. */\n\tprivate syncFrameDepth = 0;\n\tprivate operationId: string | undefined;\n\tprivate eventType: string | undefined;\n\n\tsubscribe(listener: SubscriberListener<TEvent>): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => this.listeners.delete(listener);\n\t}\n\n\t/** Deliver `event` to every subscriber in order; a throwing subscriber fails the emission as `hook`. */\n\tasync emit(event: TEvent, currentOperationId: string | undefined, signal?: AbortSignal): Promise<void> {\n\t\tfor (const listener of this.listeners) {\n\t\t\tconst previousOperationId = this.operationId;\n\t\t\tconst previousEventType = this.eventType;\n\t\t\tthis.operationId = currentOperationId;\n\t\t\tthis.eventType = event.type;\n\t\t\tlet pending: Promise<unknown> | unknown;\n\t\t\tthis.syncFrameDepth += 1;\n\t\t\ttry {\n\t\t\t\tpending = listener(event, signal);\n\t\t\t} catch (error) {\n\t\t\t\tthis.operationId = previousOperationId;\n\t\t\t\tthis.eventType = previousEventType;\n\t\t\t\tthrow normalizeHarnessError(error, \"hook\");\n\t\t\t} finally {\n\t\t\t\tthis.syncFrameDepth -= 1;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait pending;\n\t\t\t} catch (error) {\n\t\t\t\tthrow normalizeHarnessError(error, \"hook\");\n\t\t\t} finally {\n\t\t\t\tthis.operationId = previousOperationId;\n\t\t\t\tthis.eventType = previousEventType;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Fail closed when an awaited listener tries to wait on its own operation.\n\t * Rejecting immediately turns a permanent deadlock into a classified error.\n\t *\n\t * Scope: this catches a wait issued from the listener's synchronous prologue,\n\t * which covers `await harness.waitForIdle()` / `await harness.abort()`. A wait\n\t * deferred behind an unrelated `await` inside the listener is not detected.\n\t */\n\tassertNotSelfWait(api: string, currentOperationId: string | undefined): void {\n\t\tif (this.syncFrameDepth > 0 && currentOperationId !== undefined && currentOperationId === this.operationId) {\n\t\t\tthrow new AgentHarnessError(\n\t\t\t\t\"invalid_state\",\n\t\t\t\t`${api} cannot await the current operation from an awaited ${this.eventType ?? \"unknown\"} callback`,\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-navigation helpers extracted from `AgentHarness.navigateTree`.
|
|
3
|
+
*
|
|
4
|
+
* These cover the two self-contained steps of a navigation — deciding where to
|
|
5
|
+
* land, and producing a branch summary — so the harness method is left with
|
|
6
|
+
* lifecycle staging, hook dispatch, and the session commit.
|
|
7
|
+
*/
|
|
8
|
+
import type { Model, RetryPolicy } from "omk-ai";
|
|
9
|
+
import { type SummarizationRetryEvent } from "./summarization-retry.ts";
|
|
10
|
+
import type { SessionTreeEntry } from "./types.ts";
|
|
11
|
+
export interface NavigationTarget {
|
|
12
|
+
/** Leaf the session should move to. */
|
|
13
|
+
readonly newLeafId: string | null;
|
|
14
|
+
/** Original text of a user-authored target, handed back for re-editing. */
|
|
15
|
+
readonly editorText?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Where a navigation lands. Targeting a user-authored entry rewinds to its
|
|
19
|
+
* parent and returns that entry's text, so the caller can edit and resend it;
|
|
20
|
+
* any other entry is entered directly.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveNavigationTarget(targetEntry: SessionTreeEntry, targetId: string): NavigationTarget;
|
|
23
|
+
export type BranchSummaryOutcome = {
|
|
24
|
+
readonly cancelled: true;
|
|
25
|
+
} | {
|
|
26
|
+
readonly cancelled: false;
|
|
27
|
+
readonly summary: string;
|
|
28
|
+
readonly details: unknown;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Summarize the entries being navigated away from. An aborted summary is a
|
|
32
|
+
* cancellation, not a failure; every other summarization error is classified
|
|
33
|
+
* as `branch_summary`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runBranchSummary(input: {
|
|
36
|
+
readonly entries: SessionTreeEntry[];
|
|
37
|
+
readonly model: Model<any>;
|
|
38
|
+
readonly apiKey: string;
|
|
39
|
+
readonly headers?: Record<string, string>;
|
|
40
|
+
readonly customInstructions?: string;
|
|
41
|
+
readonly replaceInstructions?: boolean;
|
|
42
|
+
readonly summarizationRetry: RetryPolicy | undefined;
|
|
43
|
+
readonly emit: (event: SummarizationRetryEvent) => Promise<void> | void;
|
|
44
|
+
}): Promise<BranchSummaryOutcome>;
|
|
45
|
+
//# sourceMappingURL=tree-navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-navigation.d.ts","sourceRoot":"","sources":["../../src/harness/tree-navigation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAgB,KAAK,EAAE,WAAW,EAAe,MAAM,QAAQ,CAAC;AAE5E,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAClG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAWnD,MAAM,WAAW,gBAAgB;IAChC,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAQzG;AAED,MAAM,MAAM,oBAAoB,GAC7B;IAAE,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtF;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE;IAC7C,QAAQ,CAAC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,kBAAkB,EAAE,WAAW,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAmBhC","sourcesContent":["/**\n * Tree-navigation helpers extracted from `AgentHarness.navigateTree`.\n *\n * These cover the two self-contained steps of a navigation — deciding where to\n * land, and producing a branch summary — so the harness method is left with\n * lifecycle staging, hook dispatch, and the session commit.\n */\n\nimport type { ImageContent, Model, RetryPolicy, TextContent } from \"omk-ai\";\nimport { generateBranchSummary } from \"./compaction/branch-summarization.ts\";\nimport { createSummarizationRetry, type SummarizationRetryEvent } from \"./summarization-retry.ts\";\nimport type { SessionTreeEntry } from \"./types.ts\";\nimport { AgentHarnessError } from \"./types.ts\";\n\nfunction flattenText(content: string | (TextContent | ImageContent)[]): string {\n\tif (typeof content === \"string\") return content;\n\treturn content\n\t\t.filter((part): part is TextContent => part.type === \"text\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\");\n}\n\nexport interface NavigationTarget {\n\t/** Leaf the session should move to. */\n\treadonly newLeafId: string | null;\n\t/** Original text of a user-authored target, handed back for re-editing. */\n\treadonly editorText?: string;\n}\n\n/**\n * Where a navigation lands. Targeting a user-authored entry rewinds to its\n * parent and returns that entry's text, so the caller can edit and resend it;\n * any other entry is entered directly.\n */\nexport function resolveNavigationTarget(targetEntry: SessionTreeEntry, targetId: string): NavigationTarget {\n\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\treturn { newLeafId: targetEntry.parentId, editorText: flattenText(targetEntry.message.content) };\n\t}\n\tif (targetEntry.type === \"custom_message\") {\n\t\treturn { newLeafId: targetEntry.parentId, editorText: flattenText(targetEntry.content) };\n\t}\n\treturn { newLeafId: targetId };\n}\n\nexport type BranchSummaryOutcome =\n\t| { readonly cancelled: true }\n\t| { readonly cancelled: false; readonly summary: string; readonly details: unknown };\n\n/**\n * Summarize the entries being navigated away from. An aborted summary is a\n * cancellation, not a failure; every other summarization error is classified\n * as `branch_summary`.\n */\nexport async function runBranchSummary(input: {\n\treadonly entries: SessionTreeEntry[];\n\treadonly model: Model<any>;\n\treadonly apiKey: string;\n\treadonly headers?: Record<string, string>;\n\treadonly customInstructions?: string;\n\treadonly replaceInstructions?: boolean;\n\treadonly summarizationRetry: RetryPolicy | undefined;\n\treadonly emit: (event: SummarizationRetryEvent) => Promise<void> | void;\n}): Promise<BranchSummaryOutcome> {\n\tconst result = await generateBranchSummary(input.entries, {\n\t\tmodel: input.model,\n\t\tapiKey: input.apiKey,\n\t\theaders: input.headers,\n\t\tsignal: new AbortController().signal,\n\t\tcustomInstructions: input.customInstructions,\n\t\treplaceInstructions: input.replaceInstructions,\n\t\t...createSummarizationRetry(\"branch_summary\", input.summarizationRetry, input.emit),\n\t});\n\tif (!result.ok) {\n\t\tif (result.error.code === \"aborted\") return { cancelled: true };\n\t\tthrow new AgentHarnessError(\"branch_summary\", result.error.message, result.error);\n\t}\n\treturn {\n\t\tcancelled: false,\n\t\tsummary: result.value.summary,\n\t\tdetails: { readFiles: result.value.readFiles, modifiedFiles: result.value.modifiedFiles },\n\t};\n}\n"]}
|