open-multi-agent-kit 0.96.1 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +14 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/resource-doctor-cli.d.ts +22 -0
- package/dist/commands/resource-doctor-cli.d.ts.map +1 -0
- package/dist/commands/resource-doctor-cli.js +98 -0
- package/dist/commands/resource-doctor-cli.js.map +1 -0
- package/dist/core/agent-session.d.ts +46 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +373 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/attachment-store.d.ts +39 -0
- package/dist/core/attachment-store.d.ts.map +1 -0
- package/dist/core/attachment-store.js +81 -0
- package/dist/core/attachment-store.js.map +1 -0
- package/dist/core/command-safety.d.ts +8 -0
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +140 -14
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +35 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/completion-sound.d.ts +109 -0
- package/dist/core/completion-sound.d.ts.map +1 -0
- package/dist/core/completion-sound.js +165 -0
- package/dist/core/completion-sound.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +1 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +1 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt-items.js +2 -2
- package/dist/core/context-budget-system-prompt-items.js.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +2 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +3 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +2 -1
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-scoring.d.ts +17 -0
- package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
- package/dist/core/context-budget-v2-scoring.js +25 -8
- package/dist/core/context-budget-v2-scoring.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts +2 -2
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +18 -1
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +17 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js +15 -0
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts +3 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.js +7 -0
- package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
- package/dist/core/extensions/types.d.ts +12 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/host-resource-probe.d.ts +63 -0
- package/dist/core/host-resource-probe.d.ts.map +1 -0
- package/dist/core/host-resource-probe.js +126 -0
- package/dist/core/host-resource-probe.js.map +1 -0
- package/dist/core/host-resource-snapshot.d.ts +54 -0
- package/dist/core/host-resource-snapshot.d.ts.map +1 -0
- package/dist/core/host-resource-snapshot.js +139 -0
- package/dist/core/host-resource-snapshot.js.map +1 -0
- package/dist/core/model-registry.d.ts +8 -3
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +71 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-doctor-source-scan.d.ts +9 -0
- package/dist/core/package-doctor-source-scan.d.ts.map +1 -1
- package/dist/core/package-doctor-source-scan.js +77 -4
- package/dist/core/package-doctor-source-scan.js.map +1 -1
- package/dist/core/prompt-attachment.d.ts +51 -0
- package/dist/core/prompt-attachment.d.ts.map +1 -0
- package/dist/core/prompt-attachment.js +186 -0
- package/dist/core/prompt-attachment.js.map +1 -0
- package/dist/core/prompt-settlement.d.ts +81 -0
- package/dist/core/prompt-settlement.d.ts.map +1 -0
- package/dist/core/prompt-settlement.js +84 -0
- package/dist/core/prompt-settlement.js.map +1 -0
- package/dist/core/provider-resilience.d.ts +28 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +51 -1
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-retry.d.ts +7 -0
- package/dist/core/provider-retry.d.ts.map +1 -1
- package/dist/core/provider-retry.js +24 -0
- package/dist/core/provider-retry.js.map +1 -1
- package/dist/core/provider-usage.d.ts +15 -2
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +176 -2
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/reasoning-router-promotion.d.ts +84 -0
- package/dist/core/reasoning-router-promotion.d.ts.map +1 -0
- package/dist/core/reasoning-router-promotion.js +92 -0
- package/dist/core/reasoning-router-promotion.js.map +1 -0
- package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
- package/dist/core/reasoning-router-resolver.js +1 -1
- package/dist/core/reasoning-router-resolver.js.map +1 -1
- package/dist/core/resource-admission-config.d.ts +55 -0
- package/dist/core/resource-admission-config.d.ts.map +1 -0
- package/dist/core/resource-admission-config.js +97 -0
- package/dist/core/resource-admission-config.js.map +1 -0
- package/dist/core/resource-admission.d.ts +81 -0
- package/dist/core/resource-admission.d.ts.map +1 -0
- package/dist/core/resource-admission.js +128 -0
- package/dist/core/resource-admission.js.map +1 -0
- package/dist/core/resource-governor-format.d.ts +50 -0
- package/dist/core/resource-governor-format.d.ts.map +1 -0
- package/dist/core/resource-governor-format.js +113 -0
- package/dist/core/resource-governor-format.js.map +1 -0
- package/dist/core/resource-governor-settings.d.ts +56 -0
- package/dist/core/resource-governor-settings.d.ts.map +1 -0
- package/dist/core/resource-governor-settings.js +89 -0
- package/dist/core/resource-governor-settings.js.map +1 -0
- package/dist/core/resource-observation-journal.d.ts +74 -0
- package/dist/core/resource-observation-journal.d.ts.map +1 -0
- package/dist/core/resource-observation-journal.js +198 -0
- package/dist/core/resource-observation-journal.js.map +1 -0
- package/dist/core/resource-safety-gate.d.ts +54 -0
- package/dist/core/resource-safety-gate.d.ts.map +1 -0
- package/dist/core/resource-safety-gate.js +51 -0
- package/dist/core/resource-safety-gate.js.map +1 -0
- package/dist/core/run-journal.d.ts.map +1 -1
- package/dist/core/run-journal.js +13 -1
- package/dist/core/run-journal.js.map +1 -1
- package/dist/core/run-resource-lease.d.ts +73 -0
- package/dist/core/run-resource-lease.d.ts.map +1 -0
- package/dist/core/run-resource-lease.js +78 -0
- package/dist/core/run-resource-lease.js.map +1 -0
- package/dist/core/session-bash-service.d.ts +12 -0
- package/dist/core/session-bash-service.d.ts.map +1 -1
- package/dist/core/session-bash-service.js +18 -3
- package/dist/core/session-bash-service.js.map +1 -1
- package/dist/core/session-system-prompt.d.ts +2 -0
- package/dist/core/session-system-prompt.d.ts.map +1 -1
- package/dist/core/session-system-prompt.js +2 -0
- package/dist/core/session-system-prompt.js.map +1 -1
- package/dist/core/session-termination.d.ts +8 -3
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +40 -3
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/settings-manager.d.ts +12 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +36 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/subagent-lane-launcher.d.ts +64 -0
- package/dist/core/subagent-lane-launcher.d.ts.map +1 -0
- package/dist/core/subagent-lane-launcher.js +107 -0
- package/dist/core/subagent-lane-launcher.js.map +1 -0
- package/dist/core/system-cpu-sampler.d.ts +45 -0
- package/dist/core/system-cpu-sampler.d.ts.map +1 -0
- package/dist/core/system-cpu-sampler.js +95 -0
- package/dist/core/system-cpu-sampler.js.map +1 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -8
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +3 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +13 -48
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +1 -34
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/workload-classifier.d.ts +32 -0
- package/dist/core/workload-classifier.d.ts.map +1 -0
- package/dist/core/workload-classifier.js +66 -0
- package/dist/core/workload-classifier.js.map +1 -0
- package/dist/core/workload-family-matcher.d.ts +3 -0
- package/dist/core/workload-family-matcher.d.ts.map +1 -0
- package/dist/core/workload-family-matcher.js +126 -0
- package/dist/core/workload-family-matcher.js.map +1 -0
- package/dist/core/workload-permit-pool.d.ts +78 -0
- package/dist/core/workload-permit-pool.d.ts.map +1 -0
- package/dist/core/workload-permit-pool.js +138 -0
- package/dist/core/workload-permit-pool.js.map +1 -0
- package/dist/core/workload-shard-executor.d.ts +68 -0
- package/dist/core/workload-shard-executor.d.ts.map +1 -0
- package/dist/core/workload-shard-executor.js +156 -0
- package/dist/core/workload-shard-executor.js.map +1 -0
- package/dist/core/workload-shard-plan.d.ts +81 -0
- package/dist/core/workload-shard-plan.d.ts.map +1 -0
- package/dist/core/workload-shard-plan.js +183 -0
- package/dist/core/workload-shard-plan.js.map +1 -0
- package/dist/core/workload-shard-runner.d.ts +11 -0
- package/dist/core/workload-shard-runner.d.ts.map +1 -0
- package/dist/core/workload-shard-runner.js +98 -0
- package/dist/core/workload-shard-runner.js.map +1 -0
- package/dist/core/workload-shard-store.d.ts +44 -0
- package/dist/core/workload-shard-store.d.ts.map +1 -0
- package/dist/core/workload-shard-store.js +189 -0
- package/dist/core/workload-shard-store.js.map +1 -0
- package/dist/core/workload-sharders.d.ts +47 -0
- package/dist/core/workload-sharders.d.ts.map +1 -0
- package/dist/core/workload-sharders.js +162 -0
- package/dist/core/workload-sharders.js.map +1 -0
- package/dist/core/workload-shell-scan.d.ts +12 -0
- package/dist/core/workload-shell-scan.d.ts.map +1 -0
- package/dist/core/workload-shell-scan.js +79 -0
- package/dist/core/workload-shell-scan.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/attachment-strip.d.ts +21 -0
- package/dist/modes/interactive/components/attachment-strip.d.ts.map +1 -0
- package/dist/modes/interactive/components/attachment-strip.js +136 -0
- package/dist/modes/interactive/components/attachment-strip.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +6 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +26 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.js +12 -1
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +27 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +255 -53
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/extensions.md +12 -3
- package/docs/index.md +4 -1
- package/docs/metrics.md +74 -2
- package/docs/provider-resilience.md +11 -0
- package/docs/providers.md +1 -1
- package/docs/sdk.md +4 -0
- package/docs/settings.md +64 -2
- package/docs/skills.md +16 -0
- package/docs/usage.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/agent-capability-router.test.ts +7 -0
- package/examples/extensions/subagent/domain-profiles.ts +0 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +8 -10
- package/dist/core/tools/omp-pure-seams.d.ts +0 -41
- package/dist/core/tools/omp-pure-seams.d.ts.map +0 -1
- package/dist/core/tools/omp-pure-seams.js +0 -142
- package/dist/core/tools/omp-pure-seams.js.map +0 -1
- package/dist/core/tools/omp-seam-runtime.d.ts +0 -100
- package/dist/core/tools/omp-seam-runtime.d.ts.map +0 -1
- package/dist/core/tools/omp-seam-runtime.js +0 -40
- package/dist/core/tools/omp-seam-runtime.js.map +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class prompt image attachments.
|
|
3
|
+
*
|
|
4
|
+
* Pure validation/metadata layer between clipboard/file acquisition and
|
|
5
|
+
* `AgentSession.prompt(text, { images })`. Attachments are content-addressed
|
|
6
|
+
* (sha256), validated by magic bytes — clipboard MIME metadata is never
|
|
7
|
+
* trusted — and carry decoded dimensions for preview and pixel-budget checks.
|
|
8
|
+
*/
|
|
9
|
+
export type PromptAttachmentSource = "clipboard" | "file" | "browser-capture";
|
|
10
|
+
export declare const PROMPT_IMAGE_MIME_TYPES: readonly ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
11
|
+
export type PromptImageMimeType = (typeof PROMPT_IMAGE_MIME_TYPES)[number];
|
|
12
|
+
export interface PromptImageAttachment {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly kind: "image";
|
|
15
|
+
readonly source: PromptAttachmentSource;
|
|
16
|
+
readonly mimeType: PromptImageMimeType;
|
|
17
|
+
readonly byteLength: number;
|
|
18
|
+
/** 0 when dimensions cannot be parsed from the container header. */
|
|
19
|
+
readonly width: number;
|
|
20
|
+
/** 0 when dimensions cannot be parsed from the container header. */
|
|
21
|
+
readonly height: number;
|
|
22
|
+
readonly sha256: string;
|
|
23
|
+
readonly createdAt: number;
|
|
24
|
+
}
|
|
25
|
+
/** Defaults from the attachment roadmap (§validation). */
|
|
26
|
+
export declare const PROMPT_ATTACHMENT_LIMITS: {
|
|
27
|
+
readonly maxAttachments: 8;
|
|
28
|
+
readonly maxImageBytes: number;
|
|
29
|
+
readonly maxDraftBytes: number;
|
|
30
|
+
readonly maxPixels: 40000000;
|
|
31
|
+
};
|
|
32
|
+
export type PromptAttachmentValidationCode = "empty" | "unsupported" | "too-large" | "too-many-pixels" | "limit-reached";
|
|
33
|
+
export declare class PromptAttachmentError extends Error {
|
|
34
|
+
readonly code: PromptAttachmentValidationCode;
|
|
35
|
+
constructor(code: PromptAttachmentValidationCode, message: string);
|
|
36
|
+
}
|
|
37
|
+
/** Detect the image container from magic bytes. Clipboard metadata is not trusted. */
|
|
38
|
+
export declare function sniffImageMimeType(bytes: Uint8Array): PromptImageMimeType | null;
|
|
39
|
+
export interface PromptAttachmentLimits {
|
|
40
|
+
maxImageBytes?: number;
|
|
41
|
+
maxPixels?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate raw image bytes and build the attachment record. Throws
|
|
45
|
+
* {@link PromptAttachmentError} with a stable code so callers can surface a
|
|
46
|
+
* precise message instead of silently dropping the paste.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createPromptImageAttachment(bytes: Uint8Array, source: PromptAttachmentSource, limits?: PromptAttachmentLimits): PromptImageAttachment;
|
|
49
|
+
/** Human summary for preview footers: `1280×720 · PNG · 312 KiB`. */
|
|
50
|
+
export declare function describePromptImageAttachment(attachment: PromptImageAttachment): string;
|
|
51
|
+
//# sourceMappingURL=prompt-attachment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-attachment.d.ts","sourceRoot":"","sources":["../../src/core/prompt-attachment.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,sBAAsB,GAAG,WAAW,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE9E,eAAO,MAAM,uBAAuB,iEAAkE,CAAC;AACvG,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3E,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,2DAA0D;AAC1D,eAAO,MAAM,wBAAwB;;;;;CAK3B,CAAC;AAEX,MAAM,MAAM,8BAA8B,GACvC,OAAO,GACP,aAAa,GACb,WAAW,GACX,iBAAiB,GACjB,eAAe,CAAC;AAEnB,qBAAa,qBAAsB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAE,8BAA8B,CAAC;IAE9C,YAAY,IAAI,EAAE,8BAA8B,EAAE,OAAO,EAAE,MAAM,EAIhE;CACD;AAQD,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,mBAAmB,GAAG,IAAI,CAsChF;AAoFD,MAAM,WAAW,sBAAsB;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,sBAAsB,EAC9B,MAAM,GAAE,sBAA2B,GACjC,qBAAqB,CAwCvB;AAED,wEAAqE;AACrE,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,qBAAqB,GAAG,MAAM,CAMvF","sourcesContent":["/**\n * First-class prompt image attachments.\n *\n * Pure validation/metadata layer between clipboard/file acquisition and\n * `AgentSession.prompt(text, { images })`. Attachments are content-addressed\n * (sha256), validated by magic bytes — clipboard MIME metadata is never\n * trusted — and carry decoded dimensions for preview and pixel-budget checks.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\n\nexport type PromptAttachmentSource = \"clipboard\" | \"file\" | \"browser-capture\";\n\nexport const PROMPT_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\nexport type PromptImageMimeType = (typeof PROMPT_IMAGE_MIME_TYPES)[number];\n\nexport interface PromptImageAttachment {\n\treadonly id: string;\n\treadonly kind: \"image\";\n\treadonly source: PromptAttachmentSource;\n\treadonly mimeType: PromptImageMimeType;\n\treadonly byteLength: number;\n\t/** 0 when dimensions cannot be parsed from the container header. */\n\treadonly width: number;\n\t/** 0 when dimensions cannot be parsed from the container header. */\n\treadonly height: number;\n\treadonly sha256: string;\n\treadonly createdAt: number;\n}\n\n/** Defaults from the attachment roadmap (§validation). */\nexport const PROMPT_ATTACHMENT_LIMITS = {\n\tmaxAttachments: 8,\n\tmaxImageBytes: 10 * 1024 * 1024,\n\tmaxDraftBytes: 32 * 1024 * 1024,\n\tmaxPixels: 40_000_000,\n} as const;\n\nexport type PromptAttachmentValidationCode =\n\t| \"empty\"\n\t| \"unsupported\"\n\t| \"too-large\"\n\t| \"too-many-pixels\"\n\t| \"limit-reached\";\n\nexport class PromptAttachmentError extends Error {\n\treadonly code: PromptAttachmentValidationCode;\n\n\tconstructor(code: PromptAttachmentValidationCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"PromptAttachmentError\";\n\t\tthis.code = code;\n\t}\n}\n\ninterface ImageHeaderInfo {\n\tmimeType: PromptImageMimeType;\n\twidth: number;\n\theight: number;\n}\n\n/** Detect the image container from magic bytes. Clipboard metadata is not trusted. */\nexport function sniffImageMimeType(bytes: Uint8Array): PromptImageMimeType | null {\n\tif (bytes.length < 12) return null;\n\tif (\n\t\tbytes[0] === 0x89 &&\n\t\tbytes[1] === 0x50 &&\n\t\tbytes[2] === 0x4e &&\n\t\tbytes[3] === 0x47 &&\n\t\tbytes[4] === 0x0d &&\n\t\tbytes[5] === 0x0a &&\n\t\tbytes[6] === 0x1a &&\n\t\tbytes[7] === 0x0a\n\t) {\n\t\treturn \"image/png\";\n\t}\n\tif (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return \"image/jpeg\";\n\tif (\n\t\tbytes[0] === 0x47 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x38 &&\n\t\t(bytes[4] === 0x37 || bytes[4] === 0x39) &&\n\t\tbytes[5] === 0x61\n\t) {\n\t\treturn \"image/gif\";\n\t}\n\tif (\n\t\tbytes[0] === 0x52 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x46 &&\n\t\tbytes[8] === 0x57 &&\n\t\tbytes[9] === 0x45 &&\n\t\tbytes[10] === 0x42 &&\n\t\tbytes[11] === 0x50\n\t) {\n\t\treturn \"image/webp\";\n\t}\n\treturn null;\n}\n\nfunction readPngDimensions(bytes: Uint8Array): { width: number; height: number } {\n\t// IHDR: width u32BE at 16, height u32BE at 20.\n\tif (bytes.length < 24) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\treturn { width: view.getUint32(16), height: view.getUint32(20) };\n}\n\nfunction readGifDimensions(bytes: Uint8Array): { width: number; height: number } {\n\tif (bytes.length < 10) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\treturn { width: view.getUint16(6, true), height: view.getUint16(8, true) };\n}\n\nfunction readWebpDimensions(bytes: Uint8Array): { width: number; height: number } {\n\tif (bytes.length < 30) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\tconst chunk = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);\n\tif (chunk === \"VP8X\") {\n\t\tconst width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16));\n\t\tconst height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16));\n\t\treturn { width, height };\n\t}\n\tif (chunk === \"VP8 \") {\n\t\t// Lossy bitstream: frame tag skips 3 bytes, then 3-byte start code, then dims.\n\t\tconst width = view.getUint16(26, true) & 0x3fff;\n\t\tconst height = view.getUint16(28, true) & 0x3fff;\n\t\treturn { width, height };\n\t}\n\tif (chunk === \"VP8L\") {\n\t\tconst bits = view.getUint32(21, true);\n\t\tconst width = (bits & 0x3fff) + 1;\n\t\tconst height = ((bits >> 14) & 0x3fff) + 1;\n\t\treturn { width, height };\n\t}\n\treturn { width: 0, height: 0 };\n}\n\nfunction readJpegDimensions(bytes: Uint8Array): { width: number; height: number } {\n\t// Walk SOF markers; height/width are u16BE at marker offset +5/+7.\n\tlet index = 2;\n\twhile (index + 9 < bytes.length) {\n\t\tif (bytes[index] !== 0xff) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst marker = bytes[index + 1];\n\t\tif (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n\t\t\tindex += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tconst length = (bytes[index + 2] << 8) | bytes[index + 3];\n\t\tif (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n\t\t\tconst height = (bytes[index + 5] << 8) | bytes[index + 6];\n\t\t\tconst width = (bytes[index + 7] << 8) | bytes[index + 8];\n\t\t\treturn { width, height };\n\t\t}\n\t\tindex += 2 + length;\n\t}\n\treturn { width: 0, height: 0 };\n}\n\nfunction readImageHeader(bytes: Uint8Array, mimeType: PromptImageMimeType): ImageHeaderInfo {\n\tlet dims = { width: 0, height: 0 };\n\tswitch (mimeType) {\n\t\tcase \"image/png\":\n\t\t\tdims = readPngDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/gif\":\n\t\t\tdims = readGifDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/webp\":\n\t\t\tdims = readWebpDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/jpeg\":\n\t\t\tdims = readJpegDimensions(bytes);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn { mimeType, ...dims };\n}\n\nexport interface PromptAttachmentLimits {\n\tmaxImageBytes?: number;\n\tmaxPixels?: number;\n}\n\n/**\n * Validate raw image bytes and build the attachment record. Throws\n * {@link PromptAttachmentError} with a stable code so callers can surface a\n * precise message instead of silently dropping the paste.\n */\nexport function createPromptImageAttachment(\n\tbytes: Uint8Array,\n\tsource: PromptAttachmentSource,\n\tlimits: PromptAttachmentLimits = {},\n): PromptImageAttachment {\n\tconst maxImageBytes = limits.maxImageBytes ?? PROMPT_ATTACHMENT_LIMITS.maxImageBytes;\n\tconst maxPixels = limits.maxPixels ?? PROMPT_ATTACHMENT_LIMITS.maxPixels;\n\n\tif (bytes.length === 0) {\n\t\tthrow new PromptAttachmentError(\"empty\", \"Clipboard image is empty.\");\n\t}\n\tif (bytes.length > maxImageBytes) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"too-large\",\n\t\t\t`Image is ${(bytes.length / (1024 * 1024)).toFixed(1)} MiB — limit is ${(maxImageBytes / (1024 * 1024)).toFixed(0)} MiB.`,\n\t\t);\n\t}\n\tconst mimeType = sniffImageMimeType(bytes);\n\tif (!mimeType) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"unsupported\",\n\t\t\t\"Clipboard content is not a supported image (PNG, JPEG, WebP, GIF).\",\n\t\t);\n\t}\n\tconst header = readImageHeader(bytes, mimeType);\n\tconst pixels = header.width * header.height;\n\tif (pixels > maxPixels) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"too-many-pixels\",\n\t\t\t`Image is ${header.width}×${header.height} (${(pixels / 1_000_000).toFixed(0)} MP) — limit is ${(maxPixels / 1_000_000).toFixed(0)} MP.`,\n\t\t);\n\t}\n\tconst sha256 = createHash(\"sha256\").update(bytes).digest(\"hex\");\n\treturn {\n\t\tid: randomUUID(),\n\t\tkind: \"image\",\n\t\tsource,\n\t\tmimeType,\n\t\tbyteLength: bytes.length,\n\t\twidth: header.width,\n\t\theight: header.height,\n\t\tsha256,\n\t\tcreatedAt: Date.now(),\n\t};\n}\n\n/** Human summary for preview footers: `1280×720 · PNG · 312 KiB`. */\nexport function describePromptImageAttachment(attachment: PromptImageAttachment): string {\n\tconst dims = attachment.width > 0 && attachment.height > 0 ? `${attachment.width}×${attachment.height} · ` : \"\";\n\tconst kib = attachment.byteLength / 1024;\n\tconst size = kib >= 1024 ? `${(kib / 1024).toFixed(1)} MiB` : `${Math.round(kib)} KiB`;\n\tconst mime = attachment.mimeType.replace(\"image/\", \"\").toUpperCase();\n\treturn `${dims}${mime} · ${size}`;\n}\n"]}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class prompt image attachments.
|
|
3
|
+
*
|
|
4
|
+
* Pure validation/metadata layer between clipboard/file acquisition and
|
|
5
|
+
* `AgentSession.prompt(text, { images })`. Attachments are content-addressed
|
|
6
|
+
* (sha256), validated by magic bytes — clipboard MIME metadata is never
|
|
7
|
+
* trusted — and carry decoded dimensions for preview and pixel-budget checks.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
+
export const PROMPT_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
11
|
+
/** Defaults from the attachment roadmap (§validation). */
|
|
12
|
+
export const PROMPT_ATTACHMENT_LIMITS = {
|
|
13
|
+
maxAttachments: 8,
|
|
14
|
+
maxImageBytes: 10 * 1024 * 1024,
|
|
15
|
+
maxDraftBytes: 32 * 1024 * 1024,
|
|
16
|
+
maxPixels: 40_000_000,
|
|
17
|
+
};
|
|
18
|
+
export class PromptAttachmentError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "PromptAttachmentError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Detect the image container from magic bytes. Clipboard metadata is not trusted. */
|
|
27
|
+
export function sniffImageMimeType(bytes) {
|
|
28
|
+
if (bytes.length < 12)
|
|
29
|
+
return null;
|
|
30
|
+
if (bytes[0] === 0x89 &&
|
|
31
|
+
bytes[1] === 0x50 &&
|
|
32
|
+
bytes[2] === 0x4e &&
|
|
33
|
+
bytes[3] === 0x47 &&
|
|
34
|
+
bytes[4] === 0x0d &&
|
|
35
|
+
bytes[5] === 0x0a &&
|
|
36
|
+
bytes[6] === 0x1a &&
|
|
37
|
+
bytes[7] === 0x0a) {
|
|
38
|
+
return "image/png";
|
|
39
|
+
}
|
|
40
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
|
|
41
|
+
return "image/jpeg";
|
|
42
|
+
if (bytes[0] === 0x47 &&
|
|
43
|
+
bytes[1] === 0x49 &&
|
|
44
|
+
bytes[2] === 0x46 &&
|
|
45
|
+
bytes[3] === 0x38 &&
|
|
46
|
+
(bytes[4] === 0x37 || bytes[4] === 0x39) &&
|
|
47
|
+
bytes[5] === 0x61) {
|
|
48
|
+
return "image/gif";
|
|
49
|
+
}
|
|
50
|
+
if (bytes[0] === 0x52 &&
|
|
51
|
+
bytes[1] === 0x49 &&
|
|
52
|
+
bytes[2] === 0x46 &&
|
|
53
|
+
bytes[3] === 0x46 &&
|
|
54
|
+
bytes[8] === 0x57 &&
|
|
55
|
+
bytes[9] === 0x45 &&
|
|
56
|
+
bytes[10] === 0x42 &&
|
|
57
|
+
bytes[11] === 0x50) {
|
|
58
|
+
return "image/webp";
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function readPngDimensions(bytes) {
|
|
63
|
+
// IHDR: width u32BE at 16, height u32BE at 20.
|
|
64
|
+
if (bytes.length < 24)
|
|
65
|
+
return { width: 0, height: 0 };
|
|
66
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
67
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
68
|
+
}
|
|
69
|
+
function readGifDimensions(bytes) {
|
|
70
|
+
if (bytes.length < 10)
|
|
71
|
+
return { width: 0, height: 0 };
|
|
72
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
73
|
+
return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
|
|
74
|
+
}
|
|
75
|
+
function readWebpDimensions(bytes) {
|
|
76
|
+
if (bytes.length < 30)
|
|
77
|
+
return { width: 0, height: 0 };
|
|
78
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
79
|
+
const chunk = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
|
|
80
|
+
if (chunk === "VP8X") {
|
|
81
|
+
const width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16));
|
|
82
|
+
const height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16));
|
|
83
|
+
return { width, height };
|
|
84
|
+
}
|
|
85
|
+
if (chunk === "VP8 ") {
|
|
86
|
+
// Lossy bitstream: frame tag skips 3 bytes, then 3-byte start code, then dims.
|
|
87
|
+
const width = view.getUint16(26, true) & 0x3fff;
|
|
88
|
+
const height = view.getUint16(28, true) & 0x3fff;
|
|
89
|
+
return { width, height };
|
|
90
|
+
}
|
|
91
|
+
if (chunk === "VP8L") {
|
|
92
|
+
const bits = view.getUint32(21, true);
|
|
93
|
+
const width = (bits & 0x3fff) + 1;
|
|
94
|
+
const height = ((bits >> 14) & 0x3fff) + 1;
|
|
95
|
+
return { width, height };
|
|
96
|
+
}
|
|
97
|
+
return { width: 0, height: 0 };
|
|
98
|
+
}
|
|
99
|
+
function readJpegDimensions(bytes) {
|
|
100
|
+
// Walk SOF markers; height/width are u16BE at marker offset +5/+7.
|
|
101
|
+
let index = 2;
|
|
102
|
+
while (index + 9 < bytes.length) {
|
|
103
|
+
if (bytes[index] !== 0xff) {
|
|
104
|
+
index += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const marker = bytes[index + 1];
|
|
108
|
+
if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {
|
|
109
|
+
index += 2;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const length = (bytes[index + 2] << 8) | bytes[index + 3];
|
|
113
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
114
|
+
const height = (bytes[index + 5] << 8) | bytes[index + 6];
|
|
115
|
+
const width = (bytes[index + 7] << 8) | bytes[index + 8];
|
|
116
|
+
return { width, height };
|
|
117
|
+
}
|
|
118
|
+
index += 2 + length;
|
|
119
|
+
}
|
|
120
|
+
return { width: 0, height: 0 };
|
|
121
|
+
}
|
|
122
|
+
function readImageHeader(bytes, mimeType) {
|
|
123
|
+
let dims = { width: 0, height: 0 };
|
|
124
|
+
switch (mimeType) {
|
|
125
|
+
case "image/png":
|
|
126
|
+
dims = readPngDimensions(bytes);
|
|
127
|
+
break;
|
|
128
|
+
case "image/gif":
|
|
129
|
+
dims = readGifDimensions(bytes);
|
|
130
|
+
break;
|
|
131
|
+
case "image/webp":
|
|
132
|
+
dims = readWebpDimensions(bytes);
|
|
133
|
+
break;
|
|
134
|
+
case "image/jpeg":
|
|
135
|
+
dims = readJpegDimensions(bytes);
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
return { mimeType, ...dims };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Validate raw image bytes and build the attachment record. Throws
|
|
144
|
+
* {@link PromptAttachmentError} with a stable code so callers can surface a
|
|
145
|
+
* precise message instead of silently dropping the paste.
|
|
146
|
+
*/
|
|
147
|
+
export function createPromptImageAttachment(bytes, source, limits = {}) {
|
|
148
|
+
const maxImageBytes = limits.maxImageBytes ?? PROMPT_ATTACHMENT_LIMITS.maxImageBytes;
|
|
149
|
+
const maxPixels = limits.maxPixels ?? PROMPT_ATTACHMENT_LIMITS.maxPixels;
|
|
150
|
+
if (bytes.length === 0) {
|
|
151
|
+
throw new PromptAttachmentError("empty", "Clipboard image is empty.");
|
|
152
|
+
}
|
|
153
|
+
if (bytes.length > maxImageBytes) {
|
|
154
|
+
throw new PromptAttachmentError("too-large", `Image is ${(bytes.length / (1024 * 1024)).toFixed(1)} MiB — limit is ${(maxImageBytes / (1024 * 1024)).toFixed(0)} MiB.`);
|
|
155
|
+
}
|
|
156
|
+
const mimeType = sniffImageMimeType(bytes);
|
|
157
|
+
if (!mimeType) {
|
|
158
|
+
throw new PromptAttachmentError("unsupported", "Clipboard content is not a supported image (PNG, JPEG, WebP, GIF).");
|
|
159
|
+
}
|
|
160
|
+
const header = readImageHeader(bytes, mimeType);
|
|
161
|
+
const pixels = header.width * header.height;
|
|
162
|
+
if (pixels > maxPixels) {
|
|
163
|
+
throw new PromptAttachmentError("too-many-pixels", `Image is ${header.width}×${header.height} (${(pixels / 1_000_000).toFixed(0)} MP) — limit is ${(maxPixels / 1_000_000).toFixed(0)} MP.`);
|
|
164
|
+
}
|
|
165
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
166
|
+
return {
|
|
167
|
+
id: randomUUID(),
|
|
168
|
+
kind: "image",
|
|
169
|
+
source,
|
|
170
|
+
mimeType,
|
|
171
|
+
byteLength: bytes.length,
|
|
172
|
+
width: header.width,
|
|
173
|
+
height: header.height,
|
|
174
|
+
sha256,
|
|
175
|
+
createdAt: Date.now(),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/** Human summary for preview footers: `1280×720 · PNG · 312 KiB`. */
|
|
179
|
+
export function describePromptImageAttachment(attachment) {
|
|
180
|
+
const dims = attachment.width > 0 && attachment.height > 0 ? `${attachment.width}×${attachment.height} · ` : "";
|
|
181
|
+
const kib = attachment.byteLength / 1024;
|
|
182
|
+
const size = kib >= 1024 ? `${(kib / 1024).toFixed(1)} MiB` : `${Math.round(kib)} KiB`;
|
|
183
|
+
const mime = attachment.mimeType.replace("image/", "").toUpperCase();
|
|
184
|
+
return `${dims}${mime} · ${size}`;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=prompt-attachment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-attachment.js","sourceRoot":"","sources":["../../src/core/prompt-attachment.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * First-class prompt image attachments.\n *\n * Pure validation/metadata layer between clipboard/file acquisition and\n * `AgentSession.prompt(text, { images })`. Attachments are content-addressed\n * (sha256), validated by magic bytes — clipboard MIME metadata is never\n * trusted — and carry decoded dimensions for preview and pixel-budget checks.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\n\nexport type PromptAttachmentSource = \"clipboard\" | \"file\" | \"browser-capture\";\n\nexport const PROMPT_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\nexport type PromptImageMimeType = (typeof PROMPT_IMAGE_MIME_TYPES)[number];\n\nexport interface PromptImageAttachment {\n\treadonly id: string;\n\treadonly kind: \"image\";\n\treadonly source: PromptAttachmentSource;\n\treadonly mimeType: PromptImageMimeType;\n\treadonly byteLength: number;\n\t/** 0 when dimensions cannot be parsed from the container header. */\n\treadonly width: number;\n\t/** 0 when dimensions cannot be parsed from the container header. */\n\treadonly height: number;\n\treadonly sha256: string;\n\treadonly createdAt: number;\n}\n\n/** Defaults from the attachment roadmap (§validation). */\nexport const PROMPT_ATTACHMENT_LIMITS = {\n\tmaxAttachments: 8,\n\tmaxImageBytes: 10 * 1024 * 1024,\n\tmaxDraftBytes: 32 * 1024 * 1024,\n\tmaxPixels: 40_000_000,\n} as const;\n\nexport type PromptAttachmentValidationCode =\n\t| \"empty\"\n\t| \"unsupported\"\n\t| \"too-large\"\n\t| \"too-many-pixels\"\n\t| \"limit-reached\";\n\nexport class PromptAttachmentError extends Error {\n\treadonly code: PromptAttachmentValidationCode;\n\n\tconstructor(code: PromptAttachmentValidationCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"PromptAttachmentError\";\n\t\tthis.code = code;\n\t}\n}\n\ninterface ImageHeaderInfo {\n\tmimeType: PromptImageMimeType;\n\twidth: number;\n\theight: number;\n}\n\n/** Detect the image container from magic bytes. Clipboard metadata is not trusted. */\nexport function sniffImageMimeType(bytes: Uint8Array): PromptImageMimeType | null {\n\tif (bytes.length < 12) return null;\n\tif (\n\t\tbytes[0] === 0x89 &&\n\t\tbytes[1] === 0x50 &&\n\t\tbytes[2] === 0x4e &&\n\t\tbytes[3] === 0x47 &&\n\t\tbytes[4] === 0x0d &&\n\t\tbytes[5] === 0x0a &&\n\t\tbytes[6] === 0x1a &&\n\t\tbytes[7] === 0x0a\n\t) {\n\t\treturn \"image/png\";\n\t}\n\tif (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return \"image/jpeg\";\n\tif (\n\t\tbytes[0] === 0x47 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x38 &&\n\t\t(bytes[4] === 0x37 || bytes[4] === 0x39) &&\n\t\tbytes[5] === 0x61\n\t) {\n\t\treturn \"image/gif\";\n\t}\n\tif (\n\t\tbytes[0] === 0x52 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x46 &&\n\t\tbytes[8] === 0x57 &&\n\t\tbytes[9] === 0x45 &&\n\t\tbytes[10] === 0x42 &&\n\t\tbytes[11] === 0x50\n\t) {\n\t\treturn \"image/webp\";\n\t}\n\treturn null;\n}\n\nfunction readPngDimensions(bytes: Uint8Array): { width: number; height: number } {\n\t// IHDR: width u32BE at 16, height u32BE at 20.\n\tif (bytes.length < 24) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\treturn { width: view.getUint32(16), height: view.getUint32(20) };\n}\n\nfunction readGifDimensions(bytes: Uint8Array): { width: number; height: number } {\n\tif (bytes.length < 10) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\treturn { width: view.getUint16(6, true), height: view.getUint16(8, true) };\n}\n\nfunction readWebpDimensions(bytes: Uint8Array): { width: number; height: number } {\n\tif (bytes.length < 30) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\tconst chunk = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);\n\tif (chunk === \"VP8X\") {\n\t\tconst width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16));\n\t\tconst height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16));\n\t\treturn { width, height };\n\t}\n\tif (chunk === \"VP8 \") {\n\t\t// Lossy bitstream: frame tag skips 3 bytes, then 3-byte start code, then dims.\n\t\tconst width = view.getUint16(26, true) & 0x3fff;\n\t\tconst height = view.getUint16(28, true) & 0x3fff;\n\t\treturn { width, height };\n\t}\n\tif (chunk === \"VP8L\") {\n\t\tconst bits = view.getUint32(21, true);\n\t\tconst width = (bits & 0x3fff) + 1;\n\t\tconst height = ((bits >> 14) & 0x3fff) + 1;\n\t\treturn { width, height };\n\t}\n\treturn { width: 0, height: 0 };\n}\n\nfunction readJpegDimensions(bytes: Uint8Array): { width: number; height: number } {\n\t// Walk SOF markers; height/width are u16BE at marker offset +5/+7.\n\tlet index = 2;\n\twhile (index + 9 < bytes.length) {\n\t\tif (bytes[index] !== 0xff) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst marker = bytes[index + 1];\n\t\tif (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n\t\t\tindex += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tconst length = (bytes[index + 2] << 8) | bytes[index + 3];\n\t\tif (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n\t\t\tconst height = (bytes[index + 5] << 8) | bytes[index + 6];\n\t\t\tconst width = (bytes[index + 7] << 8) | bytes[index + 8];\n\t\t\treturn { width, height };\n\t\t}\n\t\tindex += 2 + length;\n\t}\n\treturn { width: 0, height: 0 };\n}\n\nfunction readImageHeader(bytes: Uint8Array, mimeType: PromptImageMimeType): ImageHeaderInfo {\n\tlet dims = { width: 0, height: 0 };\n\tswitch (mimeType) {\n\t\tcase \"image/png\":\n\t\t\tdims = readPngDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/gif\":\n\t\t\tdims = readGifDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/webp\":\n\t\t\tdims = readWebpDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/jpeg\":\n\t\t\tdims = readJpegDimensions(bytes);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn { mimeType, ...dims };\n}\n\nexport interface PromptAttachmentLimits {\n\tmaxImageBytes?: number;\n\tmaxPixels?: number;\n}\n\n/**\n * Validate raw image bytes and build the attachment record. Throws\n * {@link PromptAttachmentError} with a stable code so callers can surface a\n * precise message instead of silently dropping the paste.\n */\nexport function createPromptImageAttachment(\n\tbytes: Uint8Array,\n\tsource: PromptAttachmentSource,\n\tlimits: PromptAttachmentLimits = {},\n): PromptImageAttachment {\n\tconst maxImageBytes = limits.maxImageBytes ?? PROMPT_ATTACHMENT_LIMITS.maxImageBytes;\n\tconst maxPixels = limits.maxPixels ?? PROMPT_ATTACHMENT_LIMITS.maxPixels;\n\n\tif (bytes.length === 0) {\n\t\tthrow new PromptAttachmentError(\"empty\", \"Clipboard image is empty.\");\n\t}\n\tif (bytes.length > maxImageBytes) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"too-large\",\n\t\t\t`Image is ${(bytes.length / (1024 * 1024)).toFixed(1)} MiB — limit is ${(maxImageBytes / (1024 * 1024)).toFixed(0)} MiB.`,\n\t\t);\n\t}\n\tconst mimeType = sniffImageMimeType(bytes);\n\tif (!mimeType) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"unsupported\",\n\t\t\t\"Clipboard content is not a supported image (PNG, JPEG, WebP, GIF).\",\n\t\t);\n\t}\n\tconst header = readImageHeader(bytes, mimeType);\n\tconst pixels = header.width * header.height;\n\tif (pixels > maxPixels) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"too-many-pixels\",\n\t\t\t`Image is ${header.width}×${header.height} (${(pixels / 1_000_000).toFixed(0)} MP) — limit is ${(maxPixels / 1_000_000).toFixed(0)} MP.`,\n\t\t);\n\t}\n\tconst sha256 = createHash(\"sha256\").update(bytes).digest(\"hex\");\n\treturn {\n\t\tid: randomUUID(),\n\t\tkind: \"image\",\n\t\tsource,\n\t\tmimeType,\n\t\tbyteLength: bytes.length,\n\t\twidth: header.width,\n\t\theight: header.height,\n\t\tsha256,\n\t\tcreatedAt: Date.now(),\n\t};\n}\n\n/** Human summary for preview footers: `1280×720 · PNG · 312 KiB`. */\nexport function describePromptImageAttachment(attachment: PromptImageAttachment): string {\n\tconst dims = attachment.width > 0 && attachment.height > 0 ? `${attachment.width}×${attachment.height} · ` : \"\";\n\tconst kib = attachment.byteLength / 1024;\n\tconst size = kib >= 1024 ? `${(kib / 1024).toFixed(1)} MiB` : `${Math.round(kib)} KiB`;\n\tconst mime = attachment.mimeType.replace(\"image/\", \"\").toUpperCase();\n\treturn `${dims}${mime} · ${size}`;\n}\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt settlement coordinator (OMK v0.97.x roadmap §16, M4/PR6).
|
|
3
|
+
*
|
|
4
|
+
* `agent_end` is NOT final (§16.1): provider retries, compaction retries,
|
|
5
|
+
* durable-goal continuations, steering/follow-up queues, shards, and child
|
|
6
|
+
* subagents may still run. `prompt_settled` fires only when the reducer's
|
|
7
|
+
* §16.4 conditions all hold, exactly once per promptRunId. Consumers (the
|
|
8
|
+
* M4 completion sound, notifications) must treat it as a UX signal, never a
|
|
9
|
+
* correctness signal (§3 non-goals).
|
|
10
|
+
*
|
|
11
|
+
* The reducer is a pure function over an immutable state (§16.5). Shard and
|
|
12
|
+
* child counters are wired by M5/M6; they participate in the contract now so
|
|
13
|
+
* those slices only add signal call sites.
|
|
14
|
+
*/
|
|
15
|
+
export type PromptSettlementOutcome = "completed" | "failed" | "aborted";
|
|
16
|
+
/** §16.2 prompt run identity. */
|
|
17
|
+
export interface PromptRunRef {
|
|
18
|
+
readonly promptRunId: string;
|
|
19
|
+
readonly sessionId: string;
|
|
20
|
+
readonly startedAt: string;
|
|
21
|
+
readonly lineageId?: string;
|
|
22
|
+
}
|
|
23
|
+
/** §16.3 settlement contract. */
|
|
24
|
+
export interface PromptSettledEvent {
|
|
25
|
+
readonly type: "prompt_settled";
|
|
26
|
+
readonly promptRunId: string;
|
|
27
|
+
readonly outcome: PromptSettlementOutcome;
|
|
28
|
+
readonly durationMs: number;
|
|
29
|
+
readonly terminationKind?: string;
|
|
30
|
+
}
|
|
31
|
+
/** §16.5 coordinator state. */
|
|
32
|
+
export interface PromptSettlementState {
|
|
33
|
+
readonly promptRunId: string;
|
|
34
|
+
readonly startedAtEpochMs: number;
|
|
35
|
+
readonly activeProviderAttempts: number;
|
|
36
|
+
readonly activeTools: number;
|
|
37
|
+
readonly activeShards: number;
|
|
38
|
+
readonly activeChildren: number;
|
|
39
|
+
readonly queuedContinuations: number;
|
|
40
|
+
readonly terminal?: {
|
|
41
|
+
readonly outcome: PromptSettlementOutcome;
|
|
42
|
+
readonly terminationKind?: string;
|
|
43
|
+
};
|
|
44
|
+
readonly emitted: boolean;
|
|
45
|
+
}
|
|
46
|
+
export type PromptSettlementSignal = {
|
|
47
|
+
readonly kind: "provider_attempt";
|
|
48
|
+
readonly delta: 1 | -1;
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: "tool";
|
|
51
|
+
readonly delta: 1 | -1;
|
|
52
|
+
} | {
|
|
53
|
+
readonly kind: "shard";
|
|
54
|
+
readonly delta: 1 | -1;
|
|
55
|
+
} | {
|
|
56
|
+
readonly kind: "child";
|
|
57
|
+
readonly delta: 1 | -1;
|
|
58
|
+
} | {
|
|
59
|
+
readonly kind: "continuation";
|
|
60
|
+
readonly delta: 1 | -1;
|
|
61
|
+
} | {
|
|
62
|
+
readonly kind: "terminal";
|
|
63
|
+
readonly outcome: PromptSettlementOutcome;
|
|
64
|
+
readonly terminationKind?: string;
|
|
65
|
+
} | {
|
|
66
|
+
readonly kind: "emitted";
|
|
67
|
+
};
|
|
68
|
+
export declare function createPromptSettlementState(promptRunId: string, startedAtEpochMs: number): PromptSettlementState;
|
|
69
|
+
/** Pure reducer (§16.5). Counters clamp at zero; terminal and emitted are latching. */
|
|
70
|
+
export declare function reducePromptSettlement(state: PromptSettlementState, signal: PromptSettlementSignal): PromptSettlementState;
|
|
71
|
+
/** §16.4: every condition must hold before `prompt_settled` may emit. */
|
|
72
|
+
export declare function shouldEmitPromptSettled(state: PromptSettlementState): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Build the event when (and only when) §16.4 holds, marking the state
|
|
75
|
+
* emitted so the same sequence can never yield a second event.
|
|
76
|
+
*/
|
|
77
|
+
export declare function settlePromptIfReady(state: PromptSettlementState, nowEpochMs: number): {
|
|
78
|
+
readonly state: PromptSettlementState;
|
|
79
|
+
readonly event: PromptSettledEvent | null;
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=prompt-settlement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-settlement.d.ts","sourceRoot":"","sources":["../../src/core/prompt-settlement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Prompt settlement coordinator (OMK v0.97.x roadmap §16, M4/PR6).\n *\n * `agent_end` is NOT final (§16.1): provider retries, compaction retries,\n * durable-goal continuations, steering/follow-up queues, shards, and child\n * subagents may still run. `prompt_settled` fires only when the reducer's\n * §16.4 conditions all hold, exactly once per promptRunId. Consumers (the\n * M4 completion sound, notifications) must treat it as a UX signal, never a\n * correctness signal (§3 non-goals).\n *\n * The reducer is a pure function over an immutable state (§16.5). Shard and\n * child counters are wired by M5/M6; they participate in the contract now so\n * those slices only add signal call sites.\n */\n\nexport type PromptSettlementOutcome = \"completed\" | \"failed\" | \"aborted\";\n\n/** §16.2 prompt run identity. */\nexport interface PromptRunRef {\n\treadonly promptRunId: string;\n\treadonly sessionId: string;\n\treadonly startedAt: string;\n\treadonly lineageId?: string;\n}\n\n/** §16.3 settlement contract. */\nexport interface PromptSettledEvent {\n\treadonly type: \"prompt_settled\";\n\treadonly promptRunId: string;\n\treadonly outcome: PromptSettlementOutcome;\n\treadonly durationMs: number;\n\treadonly terminationKind?: string;\n}\n\n/** §16.5 coordinator state. */\nexport interface PromptSettlementState {\n\treadonly promptRunId: string;\n\treadonly startedAtEpochMs: number;\n\treadonly activeProviderAttempts: number;\n\treadonly activeTools: number;\n\treadonly activeShards: number;\n\treadonly activeChildren: number;\n\treadonly queuedContinuations: number;\n\treadonly terminal?: {\n\t\treadonly outcome: PromptSettlementOutcome;\n\t\treadonly terminationKind?: string;\n\t};\n\treadonly emitted: boolean;\n}\n\nexport type PromptSettlementSignal =\n\t| { readonly kind: \"provider_attempt\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"tool\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"shard\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"child\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"continuation\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"terminal\"; readonly outcome: PromptSettlementOutcome; readonly terminationKind?: string }\n\t| { readonly kind: \"emitted\" };\n\nexport function createPromptSettlementState(promptRunId: string, startedAtEpochMs: number): PromptSettlementState {\n\treturn {\n\t\tpromptRunId,\n\t\tstartedAtEpochMs,\n\t\tactiveProviderAttempts: 0,\n\t\tactiveTools: 0,\n\t\tactiveShards: 0,\n\t\tactiveChildren: 0,\n\t\tqueuedContinuations: 0,\n\t\temitted: false,\n\t};\n}\n\n/** Pure reducer (§16.5). Counters clamp at zero; terminal and emitted are latching. */\nexport function reducePromptSettlement(\n\tstate: PromptSettlementState,\n\tsignal: PromptSettlementSignal,\n): PromptSettlementState {\n\tswitch (signal.kind) {\n\t\tcase \"provider_attempt\":\n\t\t\treturn { ...state, activeProviderAttempts: clampCount(state.activeProviderAttempts + signal.delta) };\n\t\tcase \"tool\":\n\t\t\treturn { ...state, activeTools: clampCount(state.activeTools + signal.delta) };\n\t\tcase \"shard\":\n\t\t\treturn { ...state, activeShards: clampCount(state.activeShards + signal.delta) };\n\t\tcase \"child\":\n\t\t\treturn { ...state, activeChildren: clampCount(state.activeChildren + signal.delta) };\n\t\tcase \"continuation\":\n\t\t\treturn { ...state, queuedContinuations: clampCount(state.queuedContinuations + signal.delta) };\n\t\tcase \"terminal\":\n\t\t\t// First terminal outcome wins; later reclassification never flips it (§15.3).\n\t\t\tif (state.terminal !== undefined) {\n\t\t\t\treturn state;\n\t\t\t}\n\t\t\treturn { ...state, terminal: { outcome: signal.outcome, terminationKind: signal.terminationKind } };\n\t\tcase \"emitted\":\n\t\t\treturn { ...state, emitted: true };\n\t\tdefault: {\n\t\t\tconst exhaustive: never = signal;\n\t\t\treturn exhaustive;\n\t\t}\n\t}\n}\n\n/** §16.4: every condition must hold before `prompt_settled` may emit. */\nexport function shouldEmitPromptSettled(state: PromptSettlementState): boolean {\n\treturn (\n\t\tstate.terminal !== undefined &&\n\t\t!state.emitted &&\n\t\tstate.activeProviderAttempts === 0 &&\n\t\tstate.activeTools === 0 &&\n\t\tstate.activeShards === 0 &&\n\t\tstate.activeChildren === 0 &&\n\t\tstate.queuedContinuations === 0\n\t);\n}\n\n/**\n * Build the event when (and only when) §16.4 holds, marking the state\n * emitted so the same sequence can never yield a second event.\n */\nexport function settlePromptIfReady(\n\tstate: PromptSettlementState,\n\tnowEpochMs: number,\n): { readonly state: PromptSettlementState; readonly event: PromptSettledEvent | null } {\n\tif (!shouldEmitPromptSettled(state) || state.terminal === undefined) {\n\t\treturn { state, event: null };\n\t}\n\tconst event: PromptSettledEvent = {\n\t\ttype: \"prompt_settled\",\n\t\tpromptRunId: state.promptRunId,\n\t\toutcome: state.terminal.outcome,\n\t\tdurationMs: Math.max(0, nowEpochMs - state.startedAtEpochMs),\n\t\t...(state.terminal.terminationKind !== undefined ? { terminationKind: state.terminal.terminationKind } : {}),\n\t};\n\treturn { state: reducePromptSettlement(state, { kind: \"emitted\" }), event };\n}\n\nfunction clampCount(value: number): number {\n\treturn Math.max(0, value);\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt settlement coordinator (OMK v0.97.x roadmap §16, M4/PR6).
|
|
3
|
+
*
|
|
4
|
+
* `agent_end` is NOT final (§16.1): provider retries, compaction retries,
|
|
5
|
+
* durable-goal continuations, steering/follow-up queues, shards, and child
|
|
6
|
+
* subagents may still run. `prompt_settled` fires only when the reducer's
|
|
7
|
+
* §16.4 conditions all hold, exactly once per promptRunId. Consumers (the
|
|
8
|
+
* M4 completion sound, notifications) must treat it as a UX signal, never a
|
|
9
|
+
* correctness signal (§3 non-goals).
|
|
10
|
+
*
|
|
11
|
+
* The reducer is a pure function over an immutable state (§16.5). Shard and
|
|
12
|
+
* child counters are wired by M5/M6; they participate in the contract now so
|
|
13
|
+
* those slices only add signal call sites.
|
|
14
|
+
*/
|
|
15
|
+
export function createPromptSettlementState(promptRunId, startedAtEpochMs) {
|
|
16
|
+
return {
|
|
17
|
+
promptRunId,
|
|
18
|
+
startedAtEpochMs,
|
|
19
|
+
activeProviderAttempts: 0,
|
|
20
|
+
activeTools: 0,
|
|
21
|
+
activeShards: 0,
|
|
22
|
+
activeChildren: 0,
|
|
23
|
+
queuedContinuations: 0,
|
|
24
|
+
emitted: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Pure reducer (§16.5). Counters clamp at zero; terminal and emitted are latching. */
|
|
28
|
+
export function reducePromptSettlement(state, signal) {
|
|
29
|
+
switch (signal.kind) {
|
|
30
|
+
case "provider_attempt":
|
|
31
|
+
return { ...state, activeProviderAttempts: clampCount(state.activeProviderAttempts + signal.delta) };
|
|
32
|
+
case "tool":
|
|
33
|
+
return { ...state, activeTools: clampCount(state.activeTools + signal.delta) };
|
|
34
|
+
case "shard":
|
|
35
|
+
return { ...state, activeShards: clampCount(state.activeShards + signal.delta) };
|
|
36
|
+
case "child":
|
|
37
|
+
return { ...state, activeChildren: clampCount(state.activeChildren + signal.delta) };
|
|
38
|
+
case "continuation":
|
|
39
|
+
return { ...state, queuedContinuations: clampCount(state.queuedContinuations + signal.delta) };
|
|
40
|
+
case "terminal":
|
|
41
|
+
// First terminal outcome wins; later reclassification never flips it (§15.3).
|
|
42
|
+
if (state.terminal !== undefined) {
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
return { ...state, terminal: { outcome: signal.outcome, terminationKind: signal.terminationKind } };
|
|
46
|
+
case "emitted":
|
|
47
|
+
return { ...state, emitted: true };
|
|
48
|
+
default: {
|
|
49
|
+
const exhaustive = signal;
|
|
50
|
+
return exhaustive;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** §16.4: every condition must hold before `prompt_settled` may emit. */
|
|
55
|
+
export function shouldEmitPromptSettled(state) {
|
|
56
|
+
return (state.terminal !== undefined &&
|
|
57
|
+
!state.emitted &&
|
|
58
|
+
state.activeProviderAttempts === 0 &&
|
|
59
|
+
state.activeTools === 0 &&
|
|
60
|
+
state.activeShards === 0 &&
|
|
61
|
+
state.activeChildren === 0 &&
|
|
62
|
+
state.queuedContinuations === 0);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the event when (and only when) §16.4 holds, marking the state
|
|
66
|
+
* emitted so the same sequence can never yield a second event.
|
|
67
|
+
*/
|
|
68
|
+
export function settlePromptIfReady(state, nowEpochMs) {
|
|
69
|
+
if (!shouldEmitPromptSettled(state) || state.terminal === undefined) {
|
|
70
|
+
return { state, event: null };
|
|
71
|
+
}
|
|
72
|
+
const event = {
|
|
73
|
+
type: "prompt_settled",
|
|
74
|
+
promptRunId: state.promptRunId,
|
|
75
|
+
outcome: state.terminal.outcome,
|
|
76
|
+
durationMs: Math.max(0, nowEpochMs - state.startedAtEpochMs),
|
|
77
|
+
...(state.terminal.terminationKind !== undefined ? { terminationKind: state.terminal.terminationKind } : {}),
|
|
78
|
+
};
|
|
79
|
+
return { state: reducePromptSettlement(state, { kind: "emitted" }), event };
|
|
80
|
+
}
|
|
81
|
+
function clampCount(value) {
|
|
82
|
+
return Math.max(0, value);
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=prompt-settlement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-settlement.js","sourceRoot":"","sources":["../../src/core/prompt-settlement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Prompt settlement coordinator (OMK v0.97.x roadmap §16, M4/PR6).\n *\n * `agent_end` is NOT final (§16.1): provider retries, compaction retries,\n * durable-goal continuations, steering/follow-up queues, shards, and child\n * subagents may still run. `prompt_settled` fires only when the reducer's\n * §16.4 conditions all hold, exactly once per promptRunId. Consumers (the\n * M4 completion sound, notifications) must treat it as a UX signal, never a\n * correctness signal (§3 non-goals).\n *\n * The reducer is a pure function over an immutable state (§16.5). Shard and\n * child counters are wired by M5/M6; they participate in the contract now so\n * those slices only add signal call sites.\n */\n\nexport type PromptSettlementOutcome = \"completed\" | \"failed\" | \"aborted\";\n\n/** §16.2 prompt run identity. */\nexport interface PromptRunRef {\n\treadonly promptRunId: string;\n\treadonly sessionId: string;\n\treadonly startedAt: string;\n\treadonly lineageId?: string;\n}\n\n/** §16.3 settlement contract. */\nexport interface PromptSettledEvent {\n\treadonly type: \"prompt_settled\";\n\treadonly promptRunId: string;\n\treadonly outcome: PromptSettlementOutcome;\n\treadonly durationMs: number;\n\treadonly terminationKind?: string;\n}\n\n/** §16.5 coordinator state. */\nexport interface PromptSettlementState {\n\treadonly promptRunId: string;\n\treadonly startedAtEpochMs: number;\n\treadonly activeProviderAttempts: number;\n\treadonly activeTools: number;\n\treadonly activeShards: number;\n\treadonly activeChildren: number;\n\treadonly queuedContinuations: number;\n\treadonly terminal?: {\n\t\treadonly outcome: PromptSettlementOutcome;\n\t\treadonly terminationKind?: string;\n\t};\n\treadonly emitted: boolean;\n}\n\nexport type PromptSettlementSignal =\n\t| { readonly kind: \"provider_attempt\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"tool\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"shard\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"child\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"continuation\"; readonly delta: 1 | -1 }\n\t| { readonly kind: \"terminal\"; readonly outcome: PromptSettlementOutcome; readonly terminationKind?: string }\n\t| { readonly kind: \"emitted\" };\n\nexport function createPromptSettlementState(promptRunId: string, startedAtEpochMs: number): PromptSettlementState {\n\treturn {\n\t\tpromptRunId,\n\t\tstartedAtEpochMs,\n\t\tactiveProviderAttempts: 0,\n\t\tactiveTools: 0,\n\t\tactiveShards: 0,\n\t\tactiveChildren: 0,\n\t\tqueuedContinuations: 0,\n\t\temitted: false,\n\t};\n}\n\n/** Pure reducer (§16.5). Counters clamp at zero; terminal and emitted are latching. */\nexport function reducePromptSettlement(\n\tstate: PromptSettlementState,\n\tsignal: PromptSettlementSignal,\n): PromptSettlementState {\n\tswitch (signal.kind) {\n\t\tcase \"provider_attempt\":\n\t\t\treturn { ...state, activeProviderAttempts: clampCount(state.activeProviderAttempts + signal.delta) };\n\t\tcase \"tool\":\n\t\t\treturn { ...state, activeTools: clampCount(state.activeTools + signal.delta) };\n\t\tcase \"shard\":\n\t\t\treturn { ...state, activeShards: clampCount(state.activeShards + signal.delta) };\n\t\tcase \"child\":\n\t\t\treturn { ...state, activeChildren: clampCount(state.activeChildren + signal.delta) };\n\t\tcase \"continuation\":\n\t\t\treturn { ...state, queuedContinuations: clampCount(state.queuedContinuations + signal.delta) };\n\t\tcase \"terminal\":\n\t\t\t// First terminal outcome wins; later reclassification never flips it (§15.3).\n\t\t\tif (state.terminal !== undefined) {\n\t\t\t\treturn state;\n\t\t\t}\n\t\t\treturn { ...state, terminal: { outcome: signal.outcome, terminationKind: signal.terminationKind } };\n\t\tcase \"emitted\":\n\t\t\treturn { ...state, emitted: true };\n\t\tdefault: {\n\t\t\tconst exhaustive: never = signal;\n\t\t\treturn exhaustive;\n\t\t}\n\t}\n}\n\n/** §16.4: every condition must hold before `prompt_settled` may emit. */\nexport function shouldEmitPromptSettled(state: PromptSettlementState): boolean {\n\treturn (\n\t\tstate.terminal !== undefined &&\n\t\t!state.emitted &&\n\t\tstate.activeProviderAttempts === 0 &&\n\t\tstate.activeTools === 0 &&\n\t\tstate.activeShards === 0 &&\n\t\tstate.activeChildren === 0 &&\n\t\tstate.queuedContinuations === 0\n\t);\n}\n\n/**\n * Build the event when (and only when) §16.4 holds, marking the state\n * emitted so the same sequence can never yield a second event.\n */\nexport function settlePromptIfReady(\n\tstate: PromptSettlementState,\n\tnowEpochMs: number,\n): { readonly state: PromptSettlementState; readonly event: PromptSettledEvent | null } {\n\tif (!shouldEmitPromptSettled(state) || state.terminal === undefined) {\n\t\treturn { state, event: null };\n\t}\n\tconst event: PromptSettledEvent = {\n\t\ttype: \"prompt_settled\",\n\t\tpromptRunId: state.promptRunId,\n\t\toutcome: state.terminal.outcome,\n\t\tdurationMs: Math.max(0, nowEpochMs - state.startedAtEpochMs),\n\t\t...(state.terminal.terminationKind !== undefined ? { terminationKind: state.terminal.terminationKind } : {}),\n\t};\n\treturn { state: reducePromptSettlement(state, { kind: \"emitted\" }), event };\n}\n\nfunction clampCount(value: number): number {\n\treturn Math.max(0, value);\n}\n"]}
|
|
@@ -39,6 +39,34 @@ export declare function isContentSafetyStopMessage(text: string | undefined): bo
|
|
|
39
39
|
export declare function isQuotaExhaustionMessage(text: string | undefined): boolean;
|
|
40
40
|
/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */
|
|
41
41
|
export declare function isOrphanToolCallIdError(text: string | undefined): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Transient upstream availability failures: gateway 5xx passthroughs
|
|
44
|
+
* ("503 Upstream request failed", "Endpoint is unavailable") and dropped
|
|
45
|
+
* streams that never carried a finish reason ("Stream ended without
|
|
46
|
+
* finish_reason"). Transport problems, not transcript-shape problems — they
|
|
47
|
+
* heal by backoff/retry or switching model, never by transcript sanitization.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isUpstreamUnavailableMessage(text: string | undefined): boolean;
|
|
50
|
+
/** Family key for a model route, or undefined when it has no known alias group. */
|
|
51
|
+
export declare function modelRouteFamily(model: {
|
|
52
|
+
readonly provider?: string | undefined;
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly name?: string | undefined;
|
|
55
|
+
}): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Alternative provider routes serving the same model family as
|
|
58
|
+
* {@link failed}, in registry order, excluding the failed route itself.
|
|
59
|
+
* Callers filter by authentication and per-turn failure bookkeeping.
|
|
60
|
+
*/
|
|
61
|
+
export declare function sameModelRouteCandidates<T extends {
|
|
62
|
+
readonly provider: string;
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly name?: string | undefined;
|
|
65
|
+
}>(failed: {
|
|
66
|
+
readonly provider: string;
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly name?: string | undefined;
|
|
69
|
+
}, available: readonly T[]): T[];
|
|
42
70
|
/**
|
|
43
71
|
* Errors the agent loop may auto-retry (after optional failover / message sanitize).
|
|
44
72
|
* Kept in one place so agent-session and tests share the same contract.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */\nexport const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"xai\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\n/** Fable / Opus / Sonnet stay on the requested id after a safety stop. */\nexport function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (NO_SAFETY_FAILOVER_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\n/** Explicit `--model` / `--provider` pins stay on the requested model. */\nexport function shouldHonorSafetyFailover(options: {\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly modelPinned: boolean;\n\treadonly modelId?: string;\n\treadonly provider?: string;\n}): boolean {\n\tif (!options.autoFailoverOnSafetyStop || options.modelPinned) return false;\n\treturn !isNoSafetyFailoverModel(options.modelId, options.provider);\n}\n\n/** Sticky-model eject still applies unless the user pinned that model. */\nexport function shouldEjectStickySafetyModel(options: {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly modelPinned: boolean;\n}): boolean {\n\treturn options.blockStickySafetyModels && !options.modelPinned;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */\nexport const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"xai\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\n/** Fable / Opus / Sonnet stay on the requested id after a safety stop. */\nexport function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (NO_SAFETY_FAILOVER_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Transient upstream availability failures: gateway 5xx passthroughs\n * (\"503 Upstream request failed\", \"Endpoint is unavailable\") and dropped\n * streams that never carried a finish reason (\"Stream ended without\n * finish_reason\"). Transport problems, not transcript-shape problems — they\n * heal by backoff/retry or switching model, never by transcript sanitization.\n */\nexport function isUpstreamUnavailableMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /upstream(?: request)? failed|upstream connect|endpoint (?:is )?unavailable|stream ended without finish_reason|\\b50[0234]\\b/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Route families: the same underlying model served by several provider\n * routes. Matched on the model id OR display name so alias routes like\n * `openrouter/stealth/ox-alpha`, `opencode-go/ox-alpha-free`, and\n * `opencode/x-preview-f-free` (\"Ox Alpha Free (Unlimited)\") rotate as one\n * ox-alpha family.\n */\nconst MODEL_ROUTE_FAMILIES: readonly { readonly pattern: RegExp; readonly family: string }[] = [\n\t{ pattern: /(?:^|[-_/])ox[-_ ]?alpha(?:[-_/\\s(]|$)/i, family: \"ox-alpha\" },\n];\n\n/** Family key for a model route, or undefined when it has no known alias group. */\nexport function modelRouteFamily(model: {\n\treadonly provider?: string | undefined;\n\treadonly id: string;\n\treadonly name?: string | undefined;\n}): string | undefined {\n\tfor (const { pattern, family } of MODEL_ROUTE_FAMILIES) {\n\t\tif (pattern.test(model.id)) return family;\n\t\tif (model.name && pattern.test(model.name)) return family;\n\t}\n\treturn undefined;\n}\n\n/**\n * Alternative provider routes serving the same model family as\n * {@link failed}, in registry order, excluding the failed route itself.\n * Callers filter by authentication and per-turn failure bookkeeping.\n */\nexport function sameModelRouteCandidates<\n\tT extends { readonly provider: string; readonly id: string; readonly name?: string | undefined },\n>(\n\tfailed: { readonly provider: string; readonly id: string; readonly name?: string | undefined },\n\tavailable: readonly T[],\n): T[] {\n\tconst family = modelRouteFamily(failed);\n\tif (!family) return [];\n\tconst candidates: T[] = [];\n\tfor (const model of available) {\n\t\tif (model.provider === failed.provider && model.id === failed.id) continue;\n\t\tif (modelRouteFamily(model) === family) candidates.push(model);\n\t}\n\treturn candidates;\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error|json error injected into sse stream|injected into sse/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\n/** Explicit `--model` / `--provider` pins stay on the requested model. */\nexport function shouldHonorSafetyFailover(options: {\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly modelPinned: boolean;\n\treadonly modelId?: string;\n\treadonly provider?: string;\n}): boolean {\n\tif (!options.autoFailoverOnSafetyStop || options.modelPinned) return false;\n\treturn !isNoSafetyFailoverModel(options.modelId, options.provider);\n}\n\n/** Sticky-model eject still applies unless the user pinned that model. */\nexport function shouldEjectStickySafetyModel(options: {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly modelPinned: boolean;\n}): boolean {\n\treturn options.blockStickySafetyModels && !options.modelPinned;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
|