omk-agent-core 0.98.0 → 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.
Files changed (77) hide show
  1. package/dist/harness/agent-harness.d.ts +29 -14
  2. package/dist/harness/agent-harness.d.ts.map +1 -1
  3. package/dist/harness/agent-harness.js +409 -453
  4. package/dist/harness/agent-harness.js.map +1 -1
  5. package/dist/harness/compaction/branch-summarization.d.ts +5 -1
  6. package/dist/harness/compaction/branch-summarization.d.ts.map +1 -1
  7. package/dist/harness/compaction/branch-summarization.js +3 -3
  8. package/dist/harness/compaction/branch-summarization.js.map +1 -1
  9. package/dist/harness/compaction/compaction.d.ts +8 -4
  10. package/dist/harness/compaction/compaction.d.ts.map +1 -1
  11. package/dist/harness/compaction/compaction.js +12 -97
  12. package/dist/harness/compaction/compaction.js.map +1 -1
  13. package/dist/harness/compaction/operation.d.ts +13 -0
  14. package/dist/harness/compaction/operation.d.ts.map +1 -0
  15. package/dist/harness/compaction/operation.js +2 -0
  16. package/dist/harness/compaction/operation.js.map +1 -0
  17. package/dist/harness/compaction/summarization-prompts.d.ts +5 -0
  18. package/dist/harness/compaction/summarization-prompts.d.ts.map +1 -0
  19. package/dist/harness/compaction/summarization-prompts.js +88 -0
  20. package/dist/harness/compaction/summarization-prompts.js.map +1 -0
  21. package/dist/harness/errors.d.ts +15 -0
  22. package/dist/harness/errors.d.ts.map +1 -0
  23. package/dist/harness/errors.js +32 -0
  24. package/dist/harness/errors.js.map +1 -0
  25. package/dist/harness/harness-session.d.ts +52 -0
  26. package/dist/harness/harness-session.d.ts.map +1 -0
  27. package/dist/harness/harness-session.js +113 -0
  28. package/dist/harness/harness-session.js.map +1 -0
  29. package/dist/harness/messages.d.ts +3 -1
  30. package/dist/harness/messages.d.ts.map +1 -1
  31. package/dist/harness/messages.js +26 -0
  32. package/dist/harness/messages.js.map +1 -1
  33. package/dist/harness/name-validation.d.ts +2 -0
  34. package/dist/harness/name-validation.d.ts.map +1 -0
  35. package/dist/harness/name-validation.js +11 -0
  36. package/dist/harness/name-validation.js.map +1 -0
  37. package/dist/harness/operation-lifecycle-controller.d.ts +68 -0
  38. package/dist/harness/operation-lifecycle-controller.d.ts.map +1 -0
  39. package/dist/harness/operation-lifecycle-controller.js +199 -0
  40. package/dist/harness/operation-lifecycle-controller.js.map +1 -0
  41. package/dist/harness/operation-lifecycle-reducer.d.ts +19 -0
  42. package/dist/harness/operation-lifecycle-reducer.d.ts.map +1 -0
  43. package/dist/harness/operation-lifecycle-reducer.js +201 -0
  44. package/dist/harness/operation-lifecycle-reducer.js.map +1 -0
  45. package/dist/harness/operation-lifecycle-types.d.ts +130 -0
  46. package/dist/harness/operation-lifecycle-types.d.ts.map +1 -0
  47. package/dist/harness/operation-lifecycle-types.js +34 -0
  48. package/dist/harness/operation-lifecycle-types.js.map +1 -0
  49. package/dist/harness/operation-outcome.d.ts +71 -0
  50. package/dist/harness/operation-outcome.d.ts.map +1 -0
  51. package/dist/harness/operation-outcome.js +131 -0
  52. package/dist/harness/operation-outcome.js.map +1 -0
  53. package/dist/harness/session-write-coordinator.d.ts +76 -0
  54. package/dist/harness/session-write-coordinator.d.ts.map +1 -0
  55. package/dist/harness/session-write-coordinator.js +126 -0
  56. package/dist/harness/session-write-coordinator.js.map +1 -0
  57. package/dist/harness/stream-options.d.ts +31 -0
  58. package/dist/harness/stream-options.d.ts.map +1 -0
  59. package/dist/harness/stream-options.js +66 -0
  60. package/dist/harness/stream-options.js.map +1 -0
  61. package/dist/harness/subscriber-fanout.d.ts +37 -0
  62. package/dist/harness/subscriber-fanout.d.ts.map +1 -0
  63. package/dist/harness/subscriber-fanout.js +73 -0
  64. package/dist/harness/subscriber-fanout.js.map +1 -0
  65. package/dist/harness/summarization-retry.d.ts +29 -0
  66. package/dist/harness/summarization-retry.d.ts.map +1 -0
  67. package/dist/harness/summarization-retry.js +20 -0
  68. package/dist/harness/summarization-retry.js.map +1 -0
  69. package/dist/harness/tree-navigation.d.ts +45 -0
  70. package/dist/harness/tree-navigation.d.ts.map +1 -0
  71. package/dist/harness/tree-navigation.js +59 -0
  72. package/dist/harness/tree-navigation.js.map +1 -0
  73. package/dist/harness/types.d.ts +69 -43
  74. package/dist/harness/types.d.ts.map +1 -1
  75. package/dist/harness/types.js +1 -32
  76. package/dist/harness/types.js.map +1 -1
  77. 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,31 @@
1
+ import type { RetryPolicy, SimpleStreamOptions, Transport } from "omk-ai";
2
+ /** Curated provider request options owned by the harness and snapshotted per turn. */
3
+ export interface AgentHarnessStreamOptions {
4
+ /** Preferred transport forwarded to the stream function. */
5
+ transport?: Transport;
6
+ /** Provider request timeout in milliseconds. */
7
+ timeoutMs?: number;
8
+ /** Maximum provider retry attempts. */
9
+ maxRetries?: number;
10
+ /** Optional cap for provider-requested retry delays. */
11
+ maxRetryDelayMs?: number;
12
+ /** Additional request headers merged with auth and lifecycle headers. */
13
+ headers?: Record<string, string>;
14
+ /** Provider metadata forwarded with requests. */
15
+ metadata?: SimpleStreamOptions["metadata"];
16
+ /** Provider cache retention hint. */
17
+ cacheRetention?: SimpleStreamOptions["cacheRetention"];
18
+ /** Retry policy for generated compaction and branch-summary requests. */
19
+ summarizationRetry?: RetryPolicy;
20
+ }
21
+ /** Per-request stream option patch returned by provider hooks. */
22
+ export interface AgentHarnessStreamOptionsPatch extends Omit<Partial<AgentHarnessStreamOptions>, "headers" | "metadata" | "summarizationRetry"> {
23
+ /** Header patch. `undefined` values delete keys; explicit `headers: undefined` clears all headers. */
24
+ headers?: Record<string, string | undefined>;
25
+ /** Metadata patch. `undefined` values delete keys; explicit `metadata: undefined` clears all metadata. */
26
+ metadata?: Record<string, unknown | undefined>;
27
+ }
28
+ export declare function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHarnessStreamOptions;
29
+ export declare function mergeHeaders(...headers: Array<Record<string, string> | undefined>): Record<string, string> | undefined;
30
+ export declare function applyStreamOptionsPatch(base: AgentHarnessStreamOptions, patch?: AgentHarnessStreamOptionsPatch): AgentHarnessStreamOptions;
31
+ //# sourceMappingURL=stream-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-options.d.ts","sourceRoot":"","sources":["../../src/harness/stream-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE1E,sFAAsF;AACtF,MAAM,WAAW,yBAAyB;IACzC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,iDAAiD;IACjD,QAAQ,CAAC,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAC3C,qCAAqC;IACrC,cAAc,CAAC,EAAE,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;IACvD,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,WAAW,CAAC;CACjC;AAED,kEAAkE;AAClE,MAAM,WAAW,8BAChB,SAAQ,IAAI,CAAC,OAAO,CAAC,yBAAyB,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,oBAAoB,CAAC;IAC/F,sGAAsG;IACtG,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC,CAAC;CAC/C;AAED,wBAAgB,kBAAkB,CAAC,aAAa,CAAC,EAAE,yBAAyB,GAAG,yBAAyB,CAOvG;AAED,wBAAgB,YAAY,CAC3B,GAAG,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,GACnD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CASpC;AAED,wBAAgB,uBAAuB,CACtC,IAAI,EAAE,yBAAyB,EAC/B,KAAK,CAAC,EAAE,8BAA8B,GACpC,yBAAyB,CAqC3B","sourcesContent":["import type { RetryPolicy, SimpleStreamOptions, Transport } from \"omk-ai\";\n\n/** Curated provider request options owned by the harness and snapshotted per turn. */\nexport interface AgentHarnessStreamOptions {\n\t/** Preferred transport forwarded to the stream function. */\n\ttransport?: Transport;\n\t/** Provider request timeout in milliseconds. */\n\ttimeoutMs?: number;\n\t/** Maximum provider retry attempts. */\n\tmaxRetries?: number;\n\t/** Optional cap for provider-requested retry delays. */\n\tmaxRetryDelayMs?: number;\n\t/** Additional request headers merged with auth and lifecycle headers. */\n\theaders?: Record<string, string>;\n\t/** Provider metadata forwarded with requests. */\n\tmetadata?: SimpleStreamOptions[\"metadata\"];\n\t/** Provider cache retention hint. */\n\tcacheRetention?: SimpleStreamOptions[\"cacheRetention\"];\n\t/** Retry policy for generated compaction and branch-summary requests. */\n\tsummarizationRetry?: RetryPolicy;\n}\n\n/** Per-request stream option patch returned by provider hooks. */\nexport interface AgentHarnessStreamOptionsPatch\n\textends Omit<Partial<AgentHarnessStreamOptions>, \"headers\" | \"metadata\" | \"summarizationRetry\"> {\n\t/** Header patch. `undefined` values delete keys; explicit `headers: undefined` clears all headers. */\n\theaders?: Record<string, string | undefined>;\n\t/** Metadata patch. `undefined` values delete keys; explicit `metadata: undefined` clears all metadata. */\n\tmetadata?: Record<string, unknown | undefined>;\n}\n\nexport function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHarnessStreamOptions {\n\treturn {\n\t\t...streamOptions,\n\t\theaders: streamOptions?.headers ? { ...streamOptions.headers } : undefined,\n\t\tmetadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined,\n\t\tsummarizationRetry: streamOptions?.summarizationRetry ? { ...streamOptions.summarizationRetry } : undefined,\n\t};\n}\n\nexport function mergeHeaders(\n\t...headers: Array<Record<string, string> | undefined>\n): Record<string, string> | undefined {\n\tconst merged: Record<string, string> = {};\n\tlet hasHeaders = false;\n\tfor (const entry of headers) {\n\t\tif (!entry) continue;\n\t\tObject.assign(merged, entry);\n\t\thasHeaders = true;\n\t}\n\treturn hasHeaders ? merged : undefined;\n}\n\nexport function applyStreamOptionsPatch(\n\tbase: AgentHarnessStreamOptions,\n\tpatch?: AgentHarnessStreamOptionsPatch,\n): AgentHarnessStreamOptions {\n\tconst result = cloneStreamOptions(base);\n\tif (!patch) return result;\n\n\tif (Object.hasOwn(patch, \"transport\")) result.transport = patch.transport;\n\tif (Object.hasOwn(patch, \"timeoutMs\")) result.timeoutMs = patch.timeoutMs;\n\tif (Object.hasOwn(patch, \"maxRetries\")) result.maxRetries = patch.maxRetries;\n\tif (Object.hasOwn(patch, \"maxRetryDelayMs\")) result.maxRetryDelayMs = patch.maxRetryDelayMs;\n\tif (Object.hasOwn(patch, \"cacheRetention\")) result.cacheRetention = patch.cacheRetention;\n\n\tif (Object.hasOwn(patch, \"headers\")) {\n\t\tif (patch.headers === undefined) {\n\t\t\tresult.headers = undefined;\n\t\t} else {\n\t\t\tconst headers = { ...(result.headers ?? {}) };\n\t\t\tfor (const [key, value] of Object.entries(patch.headers)) {\n\t\t\t\tif (value === undefined) delete headers[key];\n\t\t\t\telse headers[key] = value;\n\t\t\t}\n\t\t\tresult.headers = Object.keys(headers).length > 0 ? headers : undefined;\n\t\t}\n\t}\n\n\tif (Object.hasOwn(patch, \"metadata\")) {\n\t\tif (patch.metadata === undefined) {\n\t\t\tresult.metadata = undefined;\n\t\t} else {\n\t\t\tconst metadata = { ...(result.metadata ?? {}) };\n\t\t\tfor (const [key, value] of Object.entries(patch.metadata)) {\n\t\t\t\tif (value === undefined) delete metadata[key];\n\t\t\t\telse metadata[key] = value;\n\t\t\t}\n\t\t\tresult.metadata = Object.keys(metadata).length > 0 ? metadata : undefined;\n\t\t}\n\t}\n\n\treturn result;\n}\n"]}
@@ -0,0 +1,66 @@
1
+ export function cloneStreamOptions(streamOptions) {
2
+ return {
3
+ ...streamOptions,
4
+ headers: streamOptions?.headers ? { ...streamOptions.headers } : undefined,
5
+ metadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined,
6
+ summarizationRetry: streamOptions?.summarizationRetry ? { ...streamOptions.summarizationRetry } : undefined,
7
+ };
8
+ }
9
+ export function mergeHeaders(...headers) {
10
+ const merged = {};
11
+ let hasHeaders = false;
12
+ for (const entry of headers) {
13
+ if (!entry)
14
+ continue;
15
+ Object.assign(merged, entry);
16
+ hasHeaders = true;
17
+ }
18
+ return hasHeaders ? merged : undefined;
19
+ }
20
+ export function applyStreamOptionsPatch(base, patch) {
21
+ const result = cloneStreamOptions(base);
22
+ if (!patch)
23
+ return result;
24
+ if (Object.hasOwn(patch, "transport"))
25
+ result.transport = patch.transport;
26
+ if (Object.hasOwn(patch, "timeoutMs"))
27
+ result.timeoutMs = patch.timeoutMs;
28
+ if (Object.hasOwn(patch, "maxRetries"))
29
+ result.maxRetries = patch.maxRetries;
30
+ if (Object.hasOwn(patch, "maxRetryDelayMs"))
31
+ result.maxRetryDelayMs = patch.maxRetryDelayMs;
32
+ if (Object.hasOwn(patch, "cacheRetention"))
33
+ result.cacheRetention = patch.cacheRetention;
34
+ if (Object.hasOwn(patch, "headers")) {
35
+ if (patch.headers === undefined) {
36
+ result.headers = undefined;
37
+ }
38
+ else {
39
+ const headers = { ...(result.headers ?? {}) };
40
+ for (const [key, value] of Object.entries(patch.headers)) {
41
+ if (value === undefined)
42
+ delete headers[key];
43
+ else
44
+ headers[key] = value;
45
+ }
46
+ result.headers = Object.keys(headers).length > 0 ? headers : undefined;
47
+ }
48
+ }
49
+ if (Object.hasOwn(patch, "metadata")) {
50
+ if (patch.metadata === undefined) {
51
+ result.metadata = undefined;
52
+ }
53
+ else {
54
+ const metadata = { ...(result.metadata ?? {}) };
55
+ for (const [key, value] of Object.entries(patch.metadata)) {
56
+ if (value === undefined)
57
+ delete metadata[key];
58
+ else
59
+ metadata[key] = value;
60
+ }
61
+ result.metadata = Object.keys(metadata).length > 0 ? metadata : undefined;
62
+ }
63
+ }
64
+ return result;
65
+ }
66
+ //# sourceMappingURL=stream-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-options.js","sourceRoot":"","sources":["../../src/harness/stream-options.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RetryPolicy, SimpleStreamOptions, Transport } from \"omk-ai\";\n\n/** Curated provider request options owned by the harness and snapshotted per turn. */\nexport interface AgentHarnessStreamOptions {\n\t/** Preferred transport forwarded to the stream function. */\n\ttransport?: Transport;\n\t/** Provider request timeout in milliseconds. */\n\ttimeoutMs?: number;\n\t/** Maximum provider retry attempts. */\n\tmaxRetries?: number;\n\t/** Optional cap for provider-requested retry delays. */\n\tmaxRetryDelayMs?: number;\n\t/** Additional request headers merged with auth and lifecycle headers. */\n\theaders?: Record<string, string>;\n\t/** Provider metadata forwarded with requests. */\n\tmetadata?: SimpleStreamOptions[\"metadata\"];\n\t/** Provider cache retention hint. */\n\tcacheRetention?: SimpleStreamOptions[\"cacheRetention\"];\n\t/** Retry policy for generated compaction and branch-summary requests. */\n\tsummarizationRetry?: RetryPolicy;\n}\n\n/** Per-request stream option patch returned by provider hooks. */\nexport interface AgentHarnessStreamOptionsPatch\n\textends Omit<Partial<AgentHarnessStreamOptions>, \"headers\" | \"metadata\" | \"summarizationRetry\"> {\n\t/** Header patch. `undefined` values delete keys; explicit `headers: undefined` clears all headers. */\n\theaders?: Record<string, string | undefined>;\n\t/** Metadata patch. `undefined` values delete keys; explicit `metadata: undefined` clears all metadata. */\n\tmetadata?: Record<string, unknown | undefined>;\n}\n\nexport function cloneStreamOptions(streamOptions?: AgentHarnessStreamOptions): AgentHarnessStreamOptions {\n\treturn {\n\t\t...streamOptions,\n\t\theaders: streamOptions?.headers ? { ...streamOptions.headers } : undefined,\n\t\tmetadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined,\n\t\tsummarizationRetry: streamOptions?.summarizationRetry ? { ...streamOptions.summarizationRetry } : undefined,\n\t};\n}\n\nexport function mergeHeaders(\n\t...headers: Array<Record<string, string> | undefined>\n): Record<string, string> | undefined {\n\tconst merged: Record<string, string> = {};\n\tlet hasHeaders = false;\n\tfor (const entry of headers) {\n\t\tif (!entry) continue;\n\t\tObject.assign(merged, entry);\n\t\thasHeaders = true;\n\t}\n\treturn hasHeaders ? merged : undefined;\n}\n\nexport function applyStreamOptionsPatch(\n\tbase: AgentHarnessStreamOptions,\n\tpatch?: AgentHarnessStreamOptionsPatch,\n): AgentHarnessStreamOptions {\n\tconst result = cloneStreamOptions(base);\n\tif (!patch) return result;\n\n\tif (Object.hasOwn(patch, \"transport\")) result.transport = patch.transport;\n\tif (Object.hasOwn(patch, \"timeoutMs\")) result.timeoutMs = patch.timeoutMs;\n\tif (Object.hasOwn(patch, \"maxRetries\")) result.maxRetries = patch.maxRetries;\n\tif (Object.hasOwn(patch, \"maxRetryDelayMs\")) result.maxRetryDelayMs = patch.maxRetryDelayMs;\n\tif (Object.hasOwn(patch, \"cacheRetention\")) result.cacheRetention = patch.cacheRetention;\n\n\tif (Object.hasOwn(patch, \"headers\")) {\n\t\tif (patch.headers === undefined) {\n\t\t\tresult.headers = undefined;\n\t\t} else {\n\t\t\tconst headers = { ...(result.headers ?? {}) };\n\t\t\tfor (const [key, value] of Object.entries(patch.headers)) {\n\t\t\t\tif (value === undefined) delete headers[key];\n\t\t\t\telse headers[key] = value;\n\t\t\t}\n\t\t\tresult.headers = Object.keys(headers).length > 0 ? headers : undefined;\n\t\t}\n\t}\n\n\tif (Object.hasOwn(patch, \"metadata\")) {\n\t\tif (patch.metadata === undefined) {\n\t\t\tresult.metadata = undefined;\n\t\t} else {\n\t\t\tconst metadata = { ...(result.metadata ?? {}) };\n\t\t\tfor (const [key, value] of Object.entries(patch.metadata)) {\n\t\t\t\tif (value === undefined) delete metadata[key];\n\t\t\t\telse metadata[key] = value;\n\t\t\t}\n\t\t\tresult.metadata = Object.keys(metadata).length > 0 ? metadata : undefined;\n\t\t}\n\t}\n\n\treturn result;\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"]}