@wrongstack/core 0.286.0 → 0.287.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/dist/boot.d.ts.map +1 -1
- package/dist/coordination/brain-monitor.d.ts +15 -0
- package/dist/coordination/brain-monitor.d.ts.map +1 -1
- package/dist/coordination/delegate-tool.d.ts +8 -14
- package/dist/coordination/delegate-tool.d.ts.map +1 -1
- package/dist/coordination/director/director-btw-notes.d.ts +20 -0
- package/dist/coordination/director/director-btw-notes.d.ts.map +1 -0
- package/dist/coordination/director/director-collab.d.ts +40 -0
- package/dist/coordination/director/director-collab.d.ts.map +1 -0
- package/dist/coordination/director.d.ts +27 -8
- package/dist/coordination/director.d.ts.map +1 -1
- package/dist/coordination/fleet-manager.d.ts +21 -0
- package/dist/coordination/fleet-manager.d.ts.map +1 -1
- package/dist/coordination/fleet.d.ts +2 -3
- package/dist/coordination/fleet.d.ts.map +1 -1
- package/dist/coordination/global-mailbox.d.ts +36 -22
- package/dist/coordination/global-mailbox.d.ts.map +1 -1
- package/dist/coordination/icoordinator.d.ts +1 -3
- package/dist/coordination/icoordinator.d.ts.map +1 -1
- package/dist/coordination/index.js +708 -442
- package/dist/coordination/index.js.map +4 -4
- package/dist/coordination/mail-tools.d.ts.map +1 -1
- package/dist/coordination/mailbox-message-codec.d.ts +21 -1
- package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
- package/dist/coordination/mailbox-types.d.ts +42 -1
- package/dist/coordination/mailbox-types.d.ts.map +1 -1
- package/dist/core/agent-loop.d.ts.map +1 -1
- package/dist/core/agent-response.d.ts +10 -1
- package/dist/core/agent-response.d.ts.map +1 -1
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent.d.ts +8 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/context.d.ts +25 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/conversation-state.d.ts +6 -0
- package/dist/core/conversation-state.d.ts.map +1 -1
- package/dist/core/fallback-model.d.ts.map +1 -1
- package/dist/core/mailbox-loop.d.ts +12 -5
- package/dist/core/mailbox-loop.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +14 -6
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/defaults/index.d.ts +0 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +834 -1465
- package/dist/defaults/index.js.map +4 -4
- package/dist/execution/auto-compaction-middleware.d.ts +8 -1
- package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
- package/dist/execution/compaction-core.d.ts.map +1 -1
- package/dist/execution/council-brain.d.ts.map +1 -1
- package/dist/execution/council-orchestrator.d.ts +32 -0
- package/dist/execution/council-orchestrator.d.ts.map +1 -0
- package/dist/execution/council-personas.d.ts +19 -0
- package/dist/execution/council-personas.d.ts.map +1 -0
- package/dist/execution/council-profiles.d.ts +34 -0
- package/dist/execution/council-profiles.d.ts.map +1 -0
- package/dist/execution/council-prompts.d.ts +21 -0
- package/dist/execution/council-prompts.d.ts.map +1 -0
- package/dist/execution/council-resolution.d.ts +57 -0
- package/dist/execution/council-resolution.d.ts.map +1 -0
- package/dist/execution/design-kit-loader.d.ts.map +1 -1
- package/dist/execution/enhance-recovery.d.ts +35 -0
- package/dist/execution/enhance-recovery.d.ts.map +1 -0
- package/dist/execution/error-handler.d.ts.map +1 -1
- package/dist/execution/index.d.ts +6 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1738 -161
- package/dist/execution/index.js.map +4 -4
- package/dist/execution/intelligent-compactor.d.ts +18 -7
- package/dist/execution/intelligent-compactor.d.ts.map +1 -1
- package/dist/execution/one-shot-llm.d.ts +52 -0
- package/dist/execution/one-shot-llm.d.ts.map +1 -0
- package/dist/execution/prompt-enhancer.d.ts +23 -4
- package/dist/execution/prompt-enhancer.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.js +24 -6
- package/dist/execution/prompt-enhancer.js.map +2 -2
- package/dist/execution/selective-compactor.d.ts +4 -4
- package/dist/execution/strategy-compactor.d.ts +7 -0
- package/dist/execution/strategy-compactor.d.ts.map +1 -1
- package/dist/execution/tool-executor.d.ts +19 -5
- package/dist/execution/tool-executor.d.ts.map +1 -1
- package/dist/hooks/registry.d.ts.map +1 -1
- package/dist/hooks/runner.d.ts.map +1 -1
- package/dist/hooks/shell-executor.d.ts.map +1 -1
- package/dist/hq/alerts.d.ts +5 -0
- package/dist/hq/alerts.d.ts.map +1 -1
- package/dist/hq/auth-store.d.ts +17 -7
- package/dist/hq/auth-store.d.ts.map +1 -1
- package/dist/hq/commands.d.ts +8 -4
- package/dist/hq/commands.d.ts.map +1 -1
- package/dist/hq/cost-bridge.d.ts +5 -3
- package/dist/hq/cost-bridge.d.ts.map +1 -1
- package/dist/hq/fleet-bridge.d.ts.map +1 -1
- package/dist/hq/index.d.ts +0 -1
- package/dist/hq/index.d.ts.map +1 -1
- package/dist/hq/index.js +471 -294
- package/dist/hq/index.js.map +4 -4
- package/dist/hq/persistence.d.ts +42 -0
- package/dist/hq/persistence.d.ts.map +1 -1
- package/dist/hq/protocol/brain.d.ts +24 -0
- package/dist/hq/protocol/brain.d.ts.map +1 -0
- package/dist/hq/protocol/browser.d.ts +12 -0
- package/dist/hq/protocol/browser.d.ts.map +1 -0
- package/dist/hq/protocol/client.d.ts +69 -0
- package/dist/hq/protocol/client.d.ts.map +1 -0
- package/dist/hq/protocol/core.d.ts +130 -0
- package/dist/hq/protocol/core.d.ts.map +1 -0
- package/dist/hq/protocol/fleet.d.ts +73 -0
- package/dist/hq/protocol/fleet.d.ts.map +1 -0
- package/dist/hq/protocol/mailbox.d.ts +76 -0
- package/dist/hq/protocol/mailbox.d.ts.map +1 -0
- package/dist/hq/protocol/mcp.d.ts +51 -0
- package/dist/hq/protocol/mcp.d.ts.map +1 -0
- package/dist/hq/protocol/project.d.ts +26 -0
- package/dist/hq/protocol/project.d.ts.map +1 -0
- package/dist/hq/protocol/session.d.ts +153 -0
- package/dist/hq/protocol/session.d.ts.map +1 -0
- package/dist/hq/protocol/tool.d.ts +31 -0
- package/dist/hq/protocol/tool.d.ts.map +1 -0
- package/dist/hq/protocol.d.ts +10 -611
- package/dist/hq/protocol.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4540 -2283
- package/dist/index.js.map +4 -4
- package/dist/infrastructure/index.js +13 -11
- package/dist/infrastructure/index.js.map +2 -2
- package/dist/infrastructure/token-counter.d.ts.map +1 -1
- package/dist/kernel/events/agent-events.d.ts +283 -0
- package/dist/kernel/events/agent-events.d.ts.map +1 -0
- package/dist/kernel/events/brain-events.d.ts +176 -0
- package/dist/kernel/events/brain-events.d.ts.map +1 -0
- package/dist/kernel/events/fleet-events.d.ts +39 -0
- package/dist/kernel/events/fleet-events.d.ts.map +1 -0
- package/dist/kernel/events/memory-events.d.ts +165 -0
- package/dist/kernel/events/memory-events.d.ts.map +1 -0
- package/dist/kernel/events/provider-events.d.ts +98 -0
- package/dist/kernel/events/provider-events.d.ts.map +1 -0
- package/dist/kernel/events/sdd-events.d.ts +118 -0
- package/dist/kernel/events/sdd-events.d.ts.map +1 -0
- package/dist/kernel/events/session-events.d.ts +227 -0
- package/dist/kernel/events/session-events.d.ts.map +1 -0
- package/dist/kernel/events/tool-events.d.ts +185 -0
- package/dist/kernel/events/tool-events.d.ts.map +1 -0
- package/dist/kernel/events/worktree-events.d.ts +58 -0
- package/dist/kernel/events/worktree-events.d.ts.map +1 -0
- package/dist/kernel/events.d.ts +10 -1298
- package/dist/kernel/events.d.ts.map +1 -1
- package/dist/kernel/index.js +22 -2
- package/dist/kernel/index.js.map +2 -2
- package/dist/kernel/run-controller.d.ts +15 -0
- package/dist/kernel/run-controller.d.ts.map +1 -1
- package/dist/mailbox-attach.d.ts.map +1 -1
- package/dist/models/index.js +19 -6
- package/dist/models/index.js.map +2 -2
- package/dist/models/llm-selector.d.ts +7 -0
- package/dist/models/llm-selector.d.ts.map +1 -1
- package/dist/plugins/auto-review-plugin.d.ts +36 -0
- package/dist/plugins/auto-review-plugin.d.ts.map +1 -0
- package/dist/plugins/chimera-plugin.d.ts +8 -0
- package/dist/plugins/chimera-plugin.d.ts.map +1 -1
- package/dist/security/index.js +7 -2
- package/dist/security/index.js.map +2 -2
- package/dist/security/secret-scrubber.d.ts.map +1 -1
- package/dist/skills/index.js.map +2 -2
- package/dist/storage/config-loader.d.ts.map +1 -1
- package/dist/storage/goal-kanban.d.ts +69 -0
- package/dist/storage/goal-kanban.d.ts.map +1 -0
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +425 -717
- package/dist/storage/index.js.map +4 -4
- package/dist/storage/memory-consolidator.d.ts +9 -1
- package/dist/storage/memory-consolidator.d.ts.map +1 -1
- package/dist/tools/council-tool.d.ts +25 -0
- package/dist/tools/council-tool.d.ts.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1884 -12
- package/dist/tools/index.js.map +4 -4
- package/dist/tools/one-shot-llm-tool.d.ts +51 -0
- package/dist/tools/one-shot-llm-tool.d.ts.map +1 -0
- package/dist/types/config.d.ts +27 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/context-window.d.ts.map +1 -1
- package/dist/types/council.d.ts +134 -0
- package/dist/types/council.d.ts.map +1 -0
- package/dist/types/default-config.d.ts +3 -3
- package/dist/types/hooks.d.ts +14 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +29 -8
- package/dist/types/index.js.map +3 -3
- package/dist/types/one-shot-llm.d.ts +128 -0
- package/dist/types/one-shot-llm.d.ts.map +1 -0
- package/dist/types/plugin.d.ts +6 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/provider.d.ts +17 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/system-prompt.d.ts.map +1 -1
- package/dist/types/tool-executor.d.ts +10 -0
- package/dist/types/tool-executor.d.ts.map +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/index.js +75 -8
- package/dist/utils/index.js.map +3 -3
- package/dist/utils/tool-wire-compact.d.ts +1 -0
- package/dist/utils/tool-wire-compact.d.ts.map +1 -1
- package/dist/utils/wstack-paths.d.ts +20 -4
- package/dist/utils/wstack-paths.d.ts.map +1 -1
- package/instructions/cli/next-task-predictor.md +5 -3
- package/instructions/coordination/subagent-baseline.md +8 -7
- package/instructions/leader-after-task.md +18 -9
- package/instructions/llm/council-judge.md +19 -0
- package/instructions/llm/council-voter.md +22 -0
- package/instructions/modes/architect.md +14 -9
- package/instructions/modes/audit-lite.md +9 -7
- package/instructions/modes/brief-system.md +12 -12
- package/instructions/modes/brief.md +7 -5
- package/instructions/modes/code-auditor.md +13 -8
- package/instructions/modes/code-reviewer.md +13 -8
- package/instructions/modes/debug-lite.md +7 -6
- package/instructions/modes/debugger.md +13 -9
- package/instructions/modes/devops.md +13 -9
- package/instructions/modes/plan-lite.md +7 -6
- package/instructions/modes/refactor-lite.md +7 -6
- package/instructions/modes/refactorer.md +14 -9
- package/instructions/modes/research-lite.md +8 -6
- package/instructions/modes/research-web.md +18 -55
- package/instructions/modes/review-lite.md +7 -6
- package/instructions/modes/teach-system.md +15 -15
- package/instructions/modes/teach.md +13 -12
- package/instructions/modes/test-lite.md +6 -5
- package/instructions/modes/tester.md +13 -9
- package/instructions/modes/ui-design.md +14 -8
- package/instructions/sections/tool/commit-hygiene.md +1 -7
- package/instructions/sections/tool/common-patterns.md +5 -5
- package/instructions/sections/tool/delegation-full.md +3 -31
- package/instructions/sections/tool/mailbox-compact.md +1 -1
- package/instructions/sections/tool/mailbox-full.md +6 -22
- package/instructions/system.md +278 -39
- package/package.json +2 -2
- package/skills/mailbox-bridge/SKILL.md +16 -7
- package/skills/output-standards/SKILL.md +16 -16
- package/skills/wrongstack-mailbox/SKILL.md +6 -0
- package/dist/coordination/director-construction.d.ts +0 -90
- package/dist/coordination/director-construction.d.ts.map +0 -1
- package/dist/hq/agent-bridge.d.ts +0 -27
- package/dist/hq/agent-bridge.d.ts.map +0 -1
- package/dist/storage/memory-store.d.ts +0 -119
- package/dist/storage/memory-store.d.ts.map +0 -1
package/dist/tools/index.js
CHANGED
|
@@ -257,6 +257,99 @@ import * as fs2 from "node:fs/promises";
|
|
|
257
257
|
import { randomBytes } from "node:crypto";
|
|
258
258
|
import * as fs from "node:fs/promises";
|
|
259
259
|
import * as path from "node:path";
|
|
260
|
+
|
|
261
|
+
// src/utils/string.ts
|
|
262
|
+
function truncate(s, max) {
|
|
263
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/types/errors.ts
|
|
267
|
+
var ERROR_CODES = {
|
|
268
|
+
// Provider
|
|
269
|
+
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
|
|
270
|
+
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
271
|
+
PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
|
|
272
|
+
PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
|
|
273
|
+
PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
|
|
274
|
+
PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
|
|
275
|
+
PROVIDER_CONTEXT_OVERFLOW: "PROVIDER_CONTEXT_OVERFLOW",
|
|
276
|
+
// Tool
|
|
277
|
+
TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
|
|
278
|
+
TOOL_PERMISSION_DENIED: "TOOL_PERMISSION_DENIED",
|
|
279
|
+
TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
|
|
280
|
+
TOOL_TIMEOUT: "TOOL_TIMEOUT",
|
|
281
|
+
TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
|
|
282
|
+
// Config
|
|
283
|
+
CONFIG_INVALID: "CONFIG_INVALID",
|
|
284
|
+
CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
|
|
285
|
+
CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
|
|
286
|
+
CONFIG_MIGRATION_NEEDED: "CONFIG_MIGRATION_NEEDED",
|
|
287
|
+
// Plugin
|
|
288
|
+
PLUGIN_LOAD_FAILED: "PLUGIN_LOAD_FAILED",
|
|
289
|
+
PLUGIN_API_MISMATCH: "PLUGIN_API_MISMATCH",
|
|
290
|
+
PLUGIN_MISSING_DEPENDENCY: "PLUGIN_MISSING_DEPENDENCY",
|
|
291
|
+
// Agent
|
|
292
|
+
AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
|
|
293
|
+
AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
|
|
294
|
+
AGENT_ABORTED: "AGENT_ABORTED",
|
|
295
|
+
AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
|
|
296
|
+
// Session
|
|
297
|
+
SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
|
|
298
|
+
SESSION_CORRUPTED: "SESSION_CORRUPTED",
|
|
299
|
+
SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
|
|
300
|
+
// Container / Registry
|
|
301
|
+
CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
|
|
302
|
+
CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
|
|
303
|
+
CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
|
|
304
|
+
REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
|
|
305
|
+
REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
|
|
306
|
+
REGISTRY_INVALID: "REGISTRY_INVALID",
|
|
307
|
+
// File system
|
|
308
|
+
FS_READ_FAILED: "FS_READ_FAILED",
|
|
309
|
+
FS_WRITE_FAILED: "FS_WRITE_FAILED",
|
|
310
|
+
FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
|
|
311
|
+
FS_DELETE_FAILED: "FS_DELETE_FAILED",
|
|
312
|
+
FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
|
|
313
|
+
// SDD (Spec-Driven Development)
|
|
314
|
+
SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
|
|
315
|
+
SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
|
|
316
|
+
SDD_INVALID_STATE: "SDD_INVALID_STATE",
|
|
317
|
+
SDD_NOT_READY: "SDD_NOT_READY",
|
|
318
|
+
// General
|
|
319
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
320
|
+
PARSE_FAILED: "PARSE_FAILED",
|
|
321
|
+
UNKNOWN: "UNKNOWN"
|
|
322
|
+
};
|
|
323
|
+
var WrongStackError = class extends Error {
|
|
324
|
+
code;
|
|
325
|
+
subsystem;
|
|
326
|
+
severity;
|
|
327
|
+
recoverable;
|
|
328
|
+
context;
|
|
329
|
+
constructor(opts) {
|
|
330
|
+
super(opts.message, { cause: opts.cause });
|
|
331
|
+
this.name = "WrongStackError";
|
|
332
|
+
this.code = opts.code;
|
|
333
|
+
this.subsystem = opts.subsystem;
|
|
334
|
+
this.severity = opts.severity ?? "error";
|
|
335
|
+
this.recoverable = opts.recoverable ?? false;
|
|
336
|
+
this.context = opts.context;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Render a one-line user-facing description.
|
|
340
|
+
* Subclasses should override for domain-specific formatting.
|
|
341
|
+
*/
|
|
342
|
+
describe() {
|
|
343
|
+
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
344
|
+
return `${this.code}: ${this.message}${ctx}`;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
function formatContext(ctx) {
|
|
348
|
+
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
349
|
+
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/utils/atomic-write.ts
|
|
260
353
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
261
354
|
const dir = path.dirname(targetPath);
|
|
262
355
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -319,7 +412,7 @@ async function renameWithRetry(from, to) {
|
|
|
319
412
|
if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
|
|
320
413
|
throw err;
|
|
321
414
|
}
|
|
322
|
-
await new Promise((
|
|
415
|
+
await new Promise((resolve2) => setTimeout(resolve2, delays[i]));
|
|
323
416
|
}
|
|
324
417
|
}
|
|
325
418
|
throw lastErr;
|
|
@@ -344,13 +437,13 @@ async function updateJsonObjectFile(filePath, mutator) {
|
|
|
344
437
|
await writeJsonObjectFile(filePath, next);
|
|
345
438
|
return next;
|
|
346
439
|
}
|
|
347
|
-
function setJsonPath(root,
|
|
348
|
-
if (
|
|
440
|
+
function setJsonPath(root, path3, value) {
|
|
441
|
+
if (path3.length === 0) {
|
|
349
442
|
if (!isJsonObject(value)) throw new Error("Root config value must be an object");
|
|
350
443
|
return value;
|
|
351
444
|
}
|
|
352
|
-
const parent = ensureJsonParent(root,
|
|
353
|
-
const leaf = lastPathSegment(
|
|
445
|
+
const parent = ensureJsonParent(root, path3);
|
|
446
|
+
const leaf = lastPathSegment(path3);
|
|
354
447
|
if (typeof leaf === "number") {
|
|
355
448
|
if (!Array.isArray(parent)) throw new Error(`Cannot set numeric segment ${leaf} on non-array parent`);
|
|
356
449
|
parent[leaf] = value;
|
|
@@ -363,16 +456,16 @@ function setJsonPath(root, path2, value) {
|
|
|
363
456
|
function isJsonObject(value) {
|
|
364
457
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
365
458
|
}
|
|
366
|
-
function lastPathSegment(
|
|
367
|
-
const segment =
|
|
459
|
+
function lastPathSegment(path3) {
|
|
460
|
+
const segment = path3[path3.length - 1];
|
|
368
461
|
if (segment === void 0) throw new Error("Invalid empty JSON path");
|
|
369
462
|
return segment;
|
|
370
463
|
}
|
|
371
|
-
function ensureJsonParent(root,
|
|
464
|
+
function ensureJsonParent(root, path3) {
|
|
372
465
|
let current = root;
|
|
373
|
-
for (let i = 0; i <
|
|
374
|
-
const segment =
|
|
375
|
-
const nextSegment =
|
|
466
|
+
for (let i = 0; i < path3.length - 1; i += 1) {
|
|
467
|
+
const segment = path3[i];
|
|
468
|
+
const nextSegment = path3[i + 1];
|
|
376
469
|
if (segment === void 0) throw new Error("Invalid empty JSON path segment");
|
|
377
470
|
const nextContainer = typeof nextSegment === "number" ? [] : {};
|
|
378
471
|
if (typeof segment === "number") {
|
|
@@ -639,7 +732,1786 @@ function badge(state) {
|
|
|
639
732
|
return dim(state);
|
|
640
733
|
}
|
|
641
734
|
}
|
|
735
|
+
|
|
736
|
+
// src/execution/council-personas.ts
|
|
737
|
+
var PERSONA_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
738
|
+
var BUILTIN_COUNCIL_PERSONAS = Object.freeze([
|
|
739
|
+
freezePersona({
|
|
740
|
+
id: "executor",
|
|
741
|
+
name: "Executor",
|
|
742
|
+
description: "Tests whether a proposal is practical and keeps useful work moving.",
|
|
743
|
+
instruction: "Evaluate operational feasibility, concrete progress, and the cost of delay. Favor decisive action when evidence supports it, but reject action whose prerequisites are missing.",
|
|
744
|
+
tags: ["delivery", "feasibility", "progress"]
|
|
745
|
+
}),
|
|
746
|
+
freezePersona({
|
|
747
|
+
id: "skeptic",
|
|
748
|
+
name: "Skeptic",
|
|
749
|
+
description: "Challenges assumptions and looks for concrete failure modes.",
|
|
750
|
+
instruction: "Identify unsupported assumptions, unsafe premises, irreversible consequences, and credible failure modes. Oppose or refuse only when you can name a concrete reason.",
|
|
751
|
+
defaultVeto: true,
|
|
752
|
+
tags: ["risk", "assumptions", "failure-modes"]
|
|
753
|
+
}),
|
|
754
|
+
freezePersona({
|
|
755
|
+
id: "auditor",
|
|
756
|
+
name: "Auditor",
|
|
757
|
+
description: "Evaluates cost, waste, evidence quality, and expected value.",
|
|
758
|
+
instruction: "Compare resource cost, opportunity cost, evidence quality, reversibility, and expected value. Prefer the option that achieves the objective with the least avoidable waste.",
|
|
759
|
+
tags: ["cost", "evidence", "efficiency"]
|
|
760
|
+
}),
|
|
761
|
+
freezePersona({
|
|
762
|
+
id: "security",
|
|
763
|
+
name: "Security Reviewer",
|
|
764
|
+
description: "Examines trust boundaries, abuse cases, and security impact.",
|
|
765
|
+
instruction: "Evaluate trust boundaries, attacker-controlled inputs, privilege changes, data exposure, abuse cases, and recovery options. Treat unmitigated high-impact security risk as grounds to refuse.",
|
|
766
|
+
defaultVeto: true,
|
|
767
|
+
tags: ["security", "trust", "abuse-cases"]
|
|
768
|
+
}),
|
|
769
|
+
freezePersona({
|
|
770
|
+
id: "maintainer",
|
|
771
|
+
name: "Maintainer",
|
|
772
|
+
description: "Evaluates complexity, compatibility, and long-term ownership.",
|
|
773
|
+
instruction: "Evaluate behavioral compatibility, conceptual complexity, testability, migration cost, and future maintenance. Prefer the smallest design that remains clear and extensible.",
|
|
774
|
+
tags: ["maintenance", "compatibility", "simplicity"]
|
|
775
|
+
}),
|
|
776
|
+
freezePersona({
|
|
777
|
+
id: "user-advocate",
|
|
778
|
+
name: "User Advocate",
|
|
779
|
+
description: "Evaluates the decision from the affected user\u2019s perspective.",
|
|
780
|
+
instruction: "Evaluate usability, surprise, accessibility, failure recovery, and whether the outcome solves the user\u2019s stated need. Prefer understandable behavior with safe recovery paths.",
|
|
781
|
+
tags: ["users", "usability", "accessibility"]
|
|
782
|
+
})
|
|
783
|
+
]);
|
|
784
|
+
var CouncilPersonaRegistry = class _CouncilPersonaRegistry {
|
|
785
|
+
byId;
|
|
786
|
+
constructor(personas = []) {
|
|
787
|
+
const entries = /* @__PURE__ */ new Map();
|
|
788
|
+
for (const persona of personas) {
|
|
789
|
+
const normalized = freezePersona(persona);
|
|
790
|
+
if (entries.has(normalized.id)) {
|
|
791
|
+
throw new Error(`CouncilPersonaRegistry: duplicate persona id "${normalized.id}".`);
|
|
792
|
+
}
|
|
793
|
+
entries.set(normalized.id, normalized);
|
|
794
|
+
}
|
|
795
|
+
this.byId = entries;
|
|
796
|
+
}
|
|
797
|
+
has(id) {
|
|
798
|
+
return this.byId.has(id);
|
|
799
|
+
}
|
|
800
|
+
get(id) {
|
|
801
|
+
return this.byId.get(id);
|
|
802
|
+
}
|
|
803
|
+
require(id) {
|
|
804
|
+
const persona = this.get(id);
|
|
805
|
+
if (!persona) throw new Error(`CouncilPersonaRegistry: unknown persona "${id}".`);
|
|
806
|
+
return persona;
|
|
807
|
+
}
|
|
808
|
+
list() {
|
|
809
|
+
return Object.freeze([...this.byId.values()]);
|
|
810
|
+
}
|
|
811
|
+
/** Return a new registry; the current registry is never mutated. */
|
|
812
|
+
with(persona, opts = {}) {
|
|
813
|
+
const normalized = freezePersona(persona);
|
|
814
|
+
if (this.has(normalized.id) && opts.replace !== true) {
|
|
815
|
+
throw new Error(`CouncilPersonaRegistry: persona "${normalized.id}" already exists.`);
|
|
816
|
+
}
|
|
817
|
+
return new _CouncilPersonaRegistry([
|
|
818
|
+
...this.list().filter((entry) => entry.id !== normalized.id),
|
|
819
|
+
normalized
|
|
820
|
+
]);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
var DEFAULT_COUNCIL_PERSONA_REGISTRY = new CouncilPersonaRegistry(
|
|
824
|
+
BUILTIN_COUNCIL_PERSONAS
|
|
825
|
+
);
|
|
826
|
+
function freezePersona(persona) {
|
|
827
|
+
const id = persona.id.trim();
|
|
828
|
+
const name = persona.name.trim();
|
|
829
|
+
const description = persona.description.trim();
|
|
830
|
+
const instruction = persona.instruction.trim();
|
|
831
|
+
if (!PERSONA_ID_RE.test(id)) {
|
|
832
|
+
throw new Error(`CouncilPersonaRegistry: invalid persona id "${persona.id}".`);
|
|
833
|
+
}
|
|
834
|
+
if (!name) throw new Error(`CouncilPersonaRegistry: persona "${id}" requires a name.`);
|
|
835
|
+
if (!description) {
|
|
836
|
+
throw new Error(`CouncilPersonaRegistry: persona "${id}" requires a description.`);
|
|
837
|
+
}
|
|
838
|
+
if (!instruction) {
|
|
839
|
+
throw new Error(`CouncilPersonaRegistry: persona "${id}" requires an instruction.`);
|
|
840
|
+
}
|
|
841
|
+
if (persona.defaultWeight !== void 0 && (!Number.isFinite(persona.defaultWeight) || persona.defaultWeight <= 0)) {
|
|
842
|
+
throw new Error(`CouncilPersonaRegistry: persona "${id}" has an invalid default weight.`);
|
|
843
|
+
}
|
|
844
|
+
const tags = Object.freeze(
|
|
845
|
+
[...new Set((persona.tags ?? []).map((tag) => tag.trim()).filter(Boolean))]
|
|
846
|
+
);
|
|
847
|
+
return Object.freeze({
|
|
848
|
+
id,
|
|
849
|
+
name,
|
|
850
|
+
description,
|
|
851
|
+
instruction,
|
|
852
|
+
...persona.defaultWeight !== void 0 ? { defaultWeight: persona.defaultWeight } : {},
|
|
853
|
+
...persona.defaultVeto !== void 0 ? { defaultVeto: persona.defaultVeto } : {},
|
|
854
|
+
...tags.length > 0 ? { tags } : {}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/execution/council-profiles.ts
|
|
859
|
+
var PROFILE_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
860
|
+
var DISTINCTNESS_VALUES = /* @__PURE__ */ new Set([
|
|
861
|
+
"none",
|
|
862
|
+
"model",
|
|
863
|
+
"provider"
|
|
864
|
+
]);
|
|
865
|
+
var DEFAULT_COUNCIL_QUORUM_FRACTION = 0.5;
|
|
866
|
+
var DEFAULT_COUNCIL_APPROVAL_FRACTION = 0.5;
|
|
867
|
+
var DEFAULT_COUNCIL_VOTER_MAX_TOKENS = 300;
|
|
868
|
+
var DEFAULT_COUNCIL_JUDGE_MAX_TOKENS = 500;
|
|
869
|
+
var DEFAULT_COUNCIL_PER_CALL_TIMEOUT_MS = 3e4;
|
|
870
|
+
var DEFAULT_COUNCIL_OVERALL_TIMEOUT_MS = 9e4;
|
|
871
|
+
var BUILTIN_COUNCIL_PROFILES = Object.freeze([
|
|
872
|
+
Object.freeze({
|
|
873
|
+
id: "balanced",
|
|
874
|
+
name: "Balanced",
|
|
875
|
+
description: "Three complementary lenses plus an independent judge for general decisions.",
|
|
876
|
+
seats: Object.freeze([
|
|
877
|
+
Object.freeze({ persona: "executor", target: Object.freeze({ role: "planner" }) }),
|
|
878
|
+
Object.freeze({ persona: "skeptic", target: Object.freeze({ role: "critic" }) }),
|
|
879
|
+
Object.freeze({ persona: "auditor", target: Object.freeze({ role: "analyst" }) })
|
|
880
|
+
]),
|
|
881
|
+
judge: Object.freeze({ role: "reviewer" }),
|
|
882
|
+
quorumFraction: 0.5,
|
|
883
|
+
approvalFraction: 0.5,
|
|
884
|
+
distinctness: "model"
|
|
885
|
+
}),
|
|
886
|
+
Object.freeze({
|
|
887
|
+
id: "fast",
|
|
888
|
+
name: "Fast",
|
|
889
|
+
description: "Two-seat panel without a judge for quick, low-cost decisions.",
|
|
890
|
+
seats: Object.freeze([
|
|
891
|
+
Object.freeze({ persona: "executor", target: Object.freeze({ role: "planner" }) }),
|
|
892
|
+
Object.freeze({ persona: "skeptic", target: Object.freeze({ role: "critic" }) })
|
|
893
|
+
]),
|
|
894
|
+
judge: false,
|
|
895
|
+
quorumFraction: 0.5,
|
|
896
|
+
approvalFraction: 0.5,
|
|
897
|
+
distinctness: "none",
|
|
898
|
+
voterMaxTokens: 200,
|
|
899
|
+
perCallTimeoutMs: 2e4,
|
|
900
|
+
overallTimeoutMs: 3e4
|
|
901
|
+
}),
|
|
902
|
+
Object.freeze({
|
|
903
|
+
id: "risk-review",
|
|
904
|
+
name: "Risk Review",
|
|
905
|
+
description: "Security, assumptions, maintenance, and cost review for high-impact choices.",
|
|
906
|
+
seats: Object.freeze([
|
|
907
|
+
Object.freeze({ persona: "skeptic", target: Object.freeze({ role: "critic" }) }),
|
|
908
|
+
Object.freeze({ persona: "security", target: Object.freeze({ role: "security-reviewer" }) }),
|
|
909
|
+
Object.freeze({ persona: "maintainer", target: Object.freeze({ role: "reviewer" }) }),
|
|
910
|
+
Object.freeze({ persona: "auditor", target: Object.freeze({ role: "analyst" }) })
|
|
911
|
+
]),
|
|
912
|
+
judge: Object.freeze({ role: "architect" }),
|
|
913
|
+
quorumFraction: 0.75,
|
|
914
|
+
approvalFraction: 0.5,
|
|
915
|
+
distinctness: "provider",
|
|
916
|
+
judgeMaxTokens: 700,
|
|
917
|
+
overallTimeoutMs: 12e4
|
|
918
|
+
})
|
|
919
|
+
]);
|
|
920
|
+
var CouncilProfileRegistry = class _CouncilProfileRegistry {
|
|
921
|
+
byId;
|
|
922
|
+
personas;
|
|
923
|
+
constructor(profiles = [], personas = DEFAULT_COUNCIL_PERSONA_REGISTRY) {
|
|
924
|
+
this.personas = personas;
|
|
925
|
+
const entries = /* @__PURE__ */ new Map();
|
|
926
|
+
for (const profile of profiles) {
|
|
927
|
+
const normalized = normalizeCouncilProfile(profile, personas);
|
|
928
|
+
if (entries.has(normalized.id)) {
|
|
929
|
+
throw new Error(`CouncilProfileRegistry: duplicate profile id "${normalized.id}".`);
|
|
930
|
+
}
|
|
931
|
+
entries.set(normalized.id, normalized);
|
|
932
|
+
}
|
|
933
|
+
this.byId = entries;
|
|
934
|
+
}
|
|
935
|
+
has(id) {
|
|
936
|
+
return this.byId.has(id);
|
|
937
|
+
}
|
|
938
|
+
get(id) {
|
|
939
|
+
return this.byId.get(id);
|
|
940
|
+
}
|
|
941
|
+
require(id) {
|
|
942
|
+
const profile = this.get(id);
|
|
943
|
+
if (!profile) throw new Error(`CouncilProfileRegistry: unknown profile "${id}".`);
|
|
944
|
+
return profile;
|
|
945
|
+
}
|
|
946
|
+
list() {
|
|
947
|
+
return Object.freeze([...this.byId.values()]);
|
|
948
|
+
}
|
|
949
|
+
/** Return a new registry; the current registry is never mutated. */
|
|
950
|
+
with(profile, opts = {}) {
|
|
951
|
+
const personas = opts.personas ?? this.personas;
|
|
952
|
+
const normalized = normalizeCouncilProfile(profile, personas);
|
|
953
|
+
if (this.has(normalized.id) && opts.replace !== true) {
|
|
954
|
+
throw new Error(`CouncilProfileRegistry: profile "${normalized.id}" already exists.`);
|
|
955
|
+
}
|
|
956
|
+
return new _CouncilProfileRegistry(
|
|
957
|
+
[
|
|
958
|
+
...this.list().filter((entry) => entry.id !== normalized.id),
|
|
959
|
+
profile
|
|
960
|
+
],
|
|
961
|
+
personas
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
var DEFAULT_COUNCIL_PROFILE_REGISTRY = new CouncilProfileRegistry(
|
|
966
|
+
BUILTIN_COUNCIL_PROFILES
|
|
967
|
+
);
|
|
968
|
+
function resolveCouncilProfile(profile, opts = {}) {
|
|
969
|
+
const personas = opts.personas ?? DEFAULT_COUNCIL_PERSONA_REGISTRY;
|
|
970
|
+
if (profile && typeof profile !== "string") return normalizeCouncilProfile(profile, personas);
|
|
971
|
+
const id = profile ?? opts.defaultProfile ?? "balanced";
|
|
972
|
+
return (opts.registry ?? DEFAULT_COUNCIL_PROFILE_REGISTRY).require(id);
|
|
973
|
+
}
|
|
974
|
+
function normalizeCouncilProfile(profile, personas = DEFAULT_COUNCIL_PERSONA_REGISTRY) {
|
|
975
|
+
const id = profile.id.trim();
|
|
976
|
+
if (!PROFILE_ID_RE.test(id)) {
|
|
977
|
+
throw new Error(`CouncilProfileRegistry: invalid profile id "${profile.id}".`);
|
|
978
|
+
}
|
|
979
|
+
if (profile.seats.length === 0) {
|
|
980
|
+
throw new Error(`CouncilProfileRegistry: profile "${id}" requires at least one seat.`);
|
|
981
|
+
}
|
|
982
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
983
|
+
const seats = profile.seats.map((seat) => {
|
|
984
|
+
const persona = personas.require(seat.persona.trim());
|
|
985
|
+
const explicitSeatId = seat.id?.trim();
|
|
986
|
+
if (explicitSeatId && usedIds.has(explicitSeatId)) {
|
|
987
|
+
throw new Error(`CouncilProfileRegistry: duplicate seat id "${explicitSeatId}".`);
|
|
988
|
+
}
|
|
989
|
+
const seatId = uniqueSeatId(explicitSeatId || persona.id, usedIds);
|
|
990
|
+
const label = seat.label?.trim() || persona.name;
|
|
991
|
+
const weight = seat.weight ?? persona.defaultWeight ?? 1;
|
|
992
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
993
|
+
throw new Error(`CouncilProfileRegistry: seat "${seatId}" has an invalid weight.`);
|
|
994
|
+
}
|
|
995
|
+
return Object.freeze({
|
|
996
|
+
id: seatId,
|
|
997
|
+
label,
|
|
998
|
+
persona: persona.id,
|
|
999
|
+
...seat.target ? { target: freezeTarget(seat.target, `seat "${seatId}"`) } : {},
|
|
1000
|
+
weight,
|
|
1001
|
+
veto: seat.veto ?? persona.defaultVeto ?? false
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
const quorumFraction = fraction(
|
|
1005
|
+
profile.quorumFraction ?? DEFAULT_COUNCIL_QUORUM_FRACTION,
|
|
1006
|
+
"quorumFraction",
|
|
1007
|
+
id
|
|
1008
|
+
);
|
|
1009
|
+
const approvalFraction = fraction(
|
|
1010
|
+
profile.approvalFraction ?? DEFAULT_COUNCIL_APPROVAL_FRACTION,
|
|
1011
|
+
"approvalFraction",
|
|
1012
|
+
id
|
|
1013
|
+
);
|
|
1014
|
+
const distinctness = profile.distinctness ?? "model";
|
|
1015
|
+
if (!DISTINCTNESS_VALUES.has(distinctness)) {
|
|
1016
|
+
throw new Error(`CouncilProfileRegistry: profile "${id}" has invalid distinctness.`);
|
|
1017
|
+
}
|
|
1018
|
+
const voterMaxTokens = positiveInteger(
|
|
1019
|
+
profile.voterMaxTokens ?? DEFAULT_COUNCIL_VOTER_MAX_TOKENS,
|
|
1020
|
+
"voterMaxTokens",
|
|
1021
|
+
id
|
|
1022
|
+
);
|
|
1023
|
+
const judgeMaxTokens = positiveInteger(
|
|
1024
|
+
profile.judgeMaxTokens ?? DEFAULT_COUNCIL_JUDGE_MAX_TOKENS,
|
|
1025
|
+
"judgeMaxTokens",
|
|
1026
|
+
id
|
|
1027
|
+
);
|
|
1028
|
+
const perCallTimeoutMs = positiveInteger(
|
|
1029
|
+
profile.perCallTimeoutMs ?? DEFAULT_COUNCIL_PER_CALL_TIMEOUT_MS,
|
|
1030
|
+
"perCallTimeoutMs",
|
|
1031
|
+
id
|
|
1032
|
+
);
|
|
1033
|
+
const overallTimeoutMs = positiveInteger(
|
|
1034
|
+
profile.overallTimeoutMs ?? DEFAULT_COUNCIL_OVERALL_TIMEOUT_MS,
|
|
1035
|
+
"overallTimeoutMs",
|
|
1036
|
+
id
|
|
1037
|
+
);
|
|
1038
|
+
if (overallTimeoutMs < perCallTimeoutMs) {
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
`CouncilProfileRegistry: profile "${id}" overallTimeoutMs must be at least perCallTimeoutMs.`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
return Object.freeze({
|
|
1044
|
+
id,
|
|
1045
|
+
name: profile.name?.trim() || id,
|
|
1046
|
+
description: profile.description?.trim() || "",
|
|
1047
|
+
seats: Object.freeze(seats),
|
|
1048
|
+
judge: profile.judge ? freezeTarget(profile.judge, "judge") : false,
|
|
1049
|
+
quorumFraction,
|
|
1050
|
+
approvalFraction,
|
|
1051
|
+
distinctness,
|
|
1052
|
+
voterMaxTokens,
|
|
1053
|
+
judgeMaxTokens,
|
|
1054
|
+
perCallTimeoutMs,
|
|
1055
|
+
overallTimeoutMs
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
function uniqueSeatId(base, used) {
|
|
1059
|
+
if (!PROFILE_ID_RE.test(base)) {
|
|
1060
|
+
throw new Error(`CouncilProfileRegistry: invalid seat id "${base}".`);
|
|
1061
|
+
}
|
|
1062
|
+
let id = base;
|
|
1063
|
+
let suffix = 2;
|
|
1064
|
+
while (used.has(id)) id = `${base}-${suffix++}`;
|
|
1065
|
+
used.add(id);
|
|
1066
|
+
return id;
|
|
1067
|
+
}
|
|
1068
|
+
function freezeTarget(target, label) {
|
|
1069
|
+
const providerId = optionalText(target.providerId);
|
|
1070
|
+
const model = optionalText(target.model);
|
|
1071
|
+
const role = optionalText(target.role);
|
|
1072
|
+
const fallbackProfile = optionalText(target.fallbackProfile);
|
|
1073
|
+
const fallbackModels = Object.freeze(
|
|
1074
|
+
[...new Set((target.fallbackModels ?? []).map((ref) => ref.trim()).filter(Boolean))]
|
|
1075
|
+
);
|
|
1076
|
+
if (!providerId && !model && !role && !fallbackProfile && fallbackModels.length === 0) {
|
|
1077
|
+
throw new Error(`CouncilProfileRegistry: ${label} target is empty.`);
|
|
1078
|
+
}
|
|
1079
|
+
return Object.freeze({
|
|
1080
|
+
...providerId ? { providerId } : {},
|
|
1081
|
+
...model ? { model } : {},
|
|
1082
|
+
...role ? { role } : {},
|
|
1083
|
+
...fallbackProfile ? { fallbackProfile } : {},
|
|
1084
|
+
...fallbackModels.length > 0 ? { fallbackModels } : {}
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
function optionalText(value) {
|
|
1088
|
+
const trimmed = value?.trim();
|
|
1089
|
+
return trimmed || void 0;
|
|
1090
|
+
}
|
|
1091
|
+
function fraction(value, field, profileId) {
|
|
1092
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) {
|
|
1093
|
+
throw new Error(`CouncilProfileRegistry: profile "${profileId}" ${field} must be in (0, 1].`);
|
|
1094
|
+
}
|
|
1095
|
+
return value;
|
|
1096
|
+
}
|
|
1097
|
+
function positiveInteger(value, field, profileId) {
|
|
1098
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1099
|
+
throw new Error(
|
|
1100
|
+
`CouncilProfileRegistry: profile "${profileId}" ${field} must be a positive integer.`
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
return value;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/utils/instruction-file.ts
|
|
1107
|
+
import { readFileSync, statSync } from "node:fs";
|
|
1108
|
+
import * as path2 from "node:path";
|
|
1109
|
+
import { fileURLToPath } from "node:url";
|
|
1110
|
+
var textCache = /* @__PURE__ */ new Map();
|
|
1111
|
+
var rootCandidates;
|
|
1112
|
+
function readBundledInstructionText(relativePath) {
|
|
1113
|
+
const cached = textCache.get(relativePath);
|
|
1114
|
+
if (cached !== void 0) return cached;
|
|
1115
|
+
let resolved = "";
|
|
1116
|
+
for (const root of instructionRootCandidates()) {
|
|
1117
|
+
try {
|
|
1118
|
+
resolved = readFileSync(path2.join(root, relativePath), "utf8").trimEnd();
|
|
1119
|
+
break;
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
textCache.set(relativePath, resolved);
|
|
1124
|
+
return resolved;
|
|
1125
|
+
}
|
|
1126
|
+
function renderInstructionTemplate(template, values) {
|
|
1127
|
+
return template.replace(
|
|
1128
|
+
/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g,
|
|
1129
|
+
(match, key) => Object.hasOwn(values, key) ? values[key] ?? "" : match
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
function instructionRootCandidates() {
|
|
1133
|
+
if (rootCandidates !== void 0) return rootCandidates;
|
|
1134
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
1135
|
+
const candidates = [
|
|
1136
|
+
path2.resolve(here, "../../instructions"),
|
|
1137
|
+
path2.resolve(here, "../instructions"),
|
|
1138
|
+
path2.resolve(here, "instructions")
|
|
1139
|
+
];
|
|
1140
|
+
rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
|
|
1141
|
+
return rootCandidates;
|
|
1142
|
+
}
|
|
1143
|
+
function isDirectory(candidate) {
|
|
1144
|
+
try {
|
|
1145
|
+
return statSync(candidate).isDirectory();
|
|
1146
|
+
} catch {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/execution/council-prompts.ts
|
|
1152
|
+
var COUNCIL_VOTER_PROMPT_PATH = "llm/council-voter.md";
|
|
1153
|
+
var COUNCIL_JUDGE_PROMPT_PATH = "llm/council-judge.md";
|
|
1154
|
+
function buildCouncilVoterSystemPrompt(persona) {
|
|
1155
|
+
const template = requiredInstruction(COUNCIL_VOTER_PROMPT_PATH);
|
|
1156
|
+
return renderInstructionTemplate(template, {
|
|
1157
|
+
personaInstruction: persona.instruction
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
function buildCouncilJudgeSystemPrompt() {
|
|
1161
|
+
return requiredInstruction(COUNCIL_JUDGE_PROMPT_PATH);
|
|
1162
|
+
}
|
|
1163
|
+
function buildCouncilQuestionPrompt(question, opts = {}) {
|
|
1164
|
+
const text = question.question.trim();
|
|
1165
|
+
if (!text) throw new Error("buildCouncilQuestionPrompt: question must not be empty.");
|
|
1166
|
+
const options = normalizeOptions(question.options);
|
|
1167
|
+
return [
|
|
1168
|
+
"<council-question>",
|
|
1169
|
+
`Question: ${text}`,
|
|
1170
|
+
question.context?.trim() ? `Context:
|
|
1171
|
+
${question.context.trim()}` : "",
|
|
1172
|
+
options.length > 0 ? `Options (JSON):
|
|
1173
|
+
${JSON.stringify(options)}` : "Options: none",
|
|
1174
|
+
opts.refusalOptionId ? `Refusal option id: ${opts.refusalOptionId}` : "Refusal option id: not supplied",
|
|
1175
|
+
"</council-question>"
|
|
1176
|
+
].filter(Boolean).join("\n\n");
|
|
1177
|
+
}
|
|
1178
|
+
function buildCouncilVoterUserPrompt(question, seat, opts = {}) {
|
|
1179
|
+
return [
|
|
1180
|
+
buildCouncilQuestionPrompt(question, opts),
|
|
1181
|
+
"<seat-metadata>",
|
|
1182
|
+
`Seat id: ${seat.id}`,
|
|
1183
|
+
`Seat label: ${seat.label}`,
|
|
1184
|
+
`Persona id: ${seat.persona}`,
|
|
1185
|
+
"</seat-metadata>"
|
|
1186
|
+
].join("\n");
|
|
1187
|
+
}
|
|
1188
|
+
function buildCouncilJudgeUserPrompt(question, votes, opts = {}) {
|
|
1189
|
+
const ballots = votes.map((vote) => ({
|
|
1190
|
+
seatId: vote.seatId,
|
|
1191
|
+
persona: vote.persona,
|
|
1192
|
+
status: vote.status,
|
|
1193
|
+
...vote.optionId ? { optionId: vote.optionId } : {},
|
|
1194
|
+
...vote.stance ? { stance: vote.stance } : {},
|
|
1195
|
+
...vote.rationale ? { rationale: vote.rationale } : {}
|
|
1196
|
+
}));
|
|
1197
|
+
return [
|
|
1198
|
+
buildCouncilQuestionPrompt(question, { refusalOptionId: opts.refusalOptionId }),
|
|
1199
|
+
"<council-ballots>",
|
|
1200
|
+
opts.reason?.trim() ? `Reason judging is required: ${opts.reason.trim()}` : "",
|
|
1201
|
+
JSON.stringify(ballots),
|
|
1202
|
+
"</council-ballots>"
|
|
1203
|
+
].filter(Boolean).join("\n\n");
|
|
1204
|
+
}
|
|
1205
|
+
function normalizeOptions(options) {
|
|
1206
|
+
if (!options) return [];
|
|
1207
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1208
|
+
return options.map((option) => {
|
|
1209
|
+
const id = option.id.trim();
|
|
1210
|
+
const label = option.label.trim();
|
|
1211
|
+
if (!id) throw new Error("buildCouncilQuestionPrompt: option id must not be empty.");
|
|
1212
|
+
if (!label) throw new Error(`buildCouncilQuestionPrompt: option "${id}" needs a label.`);
|
|
1213
|
+
if (seen.has(id)) throw new Error(`buildCouncilQuestionPrompt: duplicate option id "${id}".`);
|
|
1214
|
+
seen.add(id);
|
|
1215
|
+
return {
|
|
1216
|
+
id,
|
|
1217
|
+
label,
|
|
1218
|
+
...option.consequence?.trim() ? { consequence: option.consequence.trim() } : {}
|
|
1219
|
+
};
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
function requiredInstruction(path3) {
|
|
1223
|
+
const text = readBundledInstructionText(path3);
|
|
1224
|
+
if (!text) throw new Error(`Council instruction file is unavailable: ${path3}`);
|
|
1225
|
+
return text;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/execution/council-resolution.ts
|
|
1229
|
+
function resolveCouncilVotes(input) {
|
|
1230
|
+
validateInput(input);
|
|
1231
|
+
const seatById = new Map(input.seats.map((seat) => [seat.id, seat]));
|
|
1232
|
+
const validVotes = [];
|
|
1233
|
+
const votedSeatIds = /* @__PURE__ */ new Set();
|
|
1234
|
+
for (const vote of input.votes) {
|
|
1235
|
+
if (!seatById.has(vote.seatId) || votedSeatIds.has(vote.seatId)) continue;
|
|
1236
|
+
votedSeatIds.add(vote.seatId);
|
|
1237
|
+
validVotes.push(vote);
|
|
1238
|
+
}
|
|
1239
|
+
if (validVotes.length / input.seats.length < input.quorumFraction) {
|
|
1240
|
+
return {
|
|
1241
|
+
status: "abstained",
|
|
1242
|
+
reason: "quorum_not_met",
|
|
1243
|
+
validVoteCount: validVotes.length,
|
|
1244
|
+
seatCount: input.seats.length
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
const veto = validVotes.find(
|
|
1248
|
+
(vote) => vote.optionId === input.refusalOptionId && seatById.get(vote.seatId)?.veto === true
|
|
1249
|
+
);
|
|
1250
|
+
if (veto) {
|
|
1251
|
+
return {
|
|
1252
|
+
status: "denied",
|
|
1253
|
+
method: "veto",
|
|
1254
|
+
optionId: veto.optionId,
|
|
1255
|
+
seatId: veto.seatId
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const weightByOption = /* @__PURE__ */ new Map();
|
|
1259
|
+
let castWeight = 0;
|
|
1260
|
+
for (const vote of validVotes) {
|
|
1261
|
+
const weight = seatById.get(vote.seatId)?.weight ?? 1;
|
|
1262
|
+
castWeight += weight;
|
|
1263
|
+
weightByOption.set(vote.optionId, (weightByOption.get(vote.optionId) ?? 0) + weight);
|
|
1264
|
+
}
|
|
1265
|
+
let winner;
|
|
1266
|
+
let contested = false;
|
|
1267
|
+
for (const [optionId, weight] of weightByOption) {
|
|
1268
|
+
if (!winner || weight > winner.weight) {
|
|
1269
|
+
winner = { optionId, weight };
|
|
1270
|
+
contested = false;
|
|
1271
|
+
} else if (weight === winner.weight) {
|
|
1272
|
+
contested = true;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const decisive = winner !== void 0 && !contested && winner.weight > input.approvalFraction * castWeight;
|
|
1276
|
+
if (!decisive || !winner) {
|
|
1277
|
+
return {
|
|
1278
|
+
status: "needs_judge",
|
|
1279
|
+
reason: contested ? "tie" : "approval_threshold_not_met",
|
|
1280
|
+
castWeight
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
if (winner.optionId === input.refusalOptionId) {
|
|
1284
|
+
return {
|
|
1285
|
+
status: "denied",
|
|
1286
|
+
method: "refusal",
|
|
1287
|
+
optionId: winner.optionId,
|
|
1288
|
+
winningWeight: winner.weight,
|
|
1289
|
+
castWeight
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
status: "decided",
|
|
1294
|
+
method: "majority",
|
|
1295
|
+
optionId: winner.optionId,
|
|
1296
|
+
winningWeight: winner.weight,
|
|
1297
|
+
castWeight
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function validateInput(input) {
|
|
1301
|
+
if (input.seats.length === 0) {
|
|
1302
|
+
throw new Error("resolveCouncilVotes: at least one seat is required.");
|
|
1303
|
+
}
|
|
1304
|
+
requireFraction(input.quorumFraction, "quorumFraction");
|
|
1305
|
+
requireFraction(input.approvalFraction, "approvalFraction");
|
|
1306
|
+
const seatIds = /* @__PURE__ */ new Set();
|
|
1307
|
+
for (const seat of input.seats) {
|
|
1308
|
+
if (!seat.id.trim()) throw new Error("resolveCouncilVotes: seat id must not be empty.");
|
|
1309
|
+
if (seatIds.has(seat.id)) {
|
|
1310
|
+
throw new Error(`resolveCouncilVotes: duplicate seat id "${seat.id}".`);
|
|
1311
|
+
}
|
|
1312
|
+
seatIds.add(seat.id);
|
|
1313
|
+
if (seat.weight !== void 0 && (!Number.isFinite(seat.weight) || seat.weight <= 0)) {
|
|
1314
|
+
throw new Error(`resolveCouncilVotes: invalid weight for seat "${seat.id}".`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function requireFraction(value, label) {
|
|
1319
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) {
|
|
1320
|
+
throw new Error(`resolveCouncilVotes: ${label} must be in (0, 1].`);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/execution/council-orchestrator.ts
|
|
1325
|
+
var COUNCIL_REFUSAL_OPTION_ID = "council_refuse";
|
|
1326
|
+
var DEFAULT_COUNCIL_MAX_CONCURRENCY = 3;
|
|
1327
|
+
var MAX_COUNCIL_CONCURRENCY = 8;
|
|
1328
|
+
var CouncilOrchestrator = class {
|
|
1329
|
+
caller;
|
|
1330
|
+
personas;
|
|
1331
|
+
profiles;
|
|
1332
|
+
defaultProfile;
|
|
1333
|
+
maxConcurrency;
|
|
1334
|
+
refusalOptionId;
|
|
1335
|
+
constructor(opts) {
|
|
1336
|
+
this.caller = opts.caller;
|
|
1337
|
+
this.personas = opts.personas ?? DEFAULT_COUNCIL_PERSONA_REGISTRY;
|
|
1338
|
+
this.profiles = opts.profiles ?? DEFAULT_COUNCIL_PROFILE_REGISTRY;
|
|
1339
|
+
this.defaultProfile = opts.defaultProfile;
|
|
1340
|
+
this.maxConcurrency = validateConcurrency(
|
|
1341
|
+
opts.maxConcurrency ?? DEFAULT_COUNCIL_MAX_CONCURRENCY
|
|
1342
|
+
);
|
|
1343
|
+
this.refusalOptionId = opts.refusalOptionId?.trim() || COUNCIL_REFUSAL_OPTION_ID;
|
|
1344
|
+
}
|
|
1345
|
+
async ask(question) {
|
|
1346
|
+
const startedAt = Date.now();
|
|
1347
|
+
const profile = resolveCouncilProfile(question.profile, {
|
|
1348
|
+
registry: this.profiles,
|
|
1349
|
+
personas: this.personas,
|
|
1350
|
+
defaultProfile: this.defaultProfile
|
|
1351
|
+
});
|
|
1352
|
+
validateRefusalCollision(question, this.refusalOptionId);
|
|
1353
|
+
const timeoutSignal = AbortSignal.timeout(profile.overallTimeoutMs);
|
|
1354
|
+
const signal = question.signal ? AbortSignal.any([question.signal, timeoutSignal]) : timeoutSignal;
|
|
1355
|
+
const usage = {
|
|
1356
|
+
calls: 0,
|
|
1357
|
+
inputTokens: 0,
|
|
1358
|
+
outputTokens: 0,
|
|
1359
|
+
totalTokens: 0
|
|
1360
|
+
};
|
|
1361
|
+
const votes = await mapConcurrent(
|
|
1362
|
+
profile.seats,
|
|
1363
|
+
Math.min(this.maxConcurrency, profile.seats.length),
|
|
1364
|
+
async (seat) => {
|
|
1365
|
+
try {
|
|
1366
|
+
return await this.callSeat(question, profile, seat, signal, usage);
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
return {
|
|
1369
|
+
seatId: seat.id,
|
|
1370
|
+
persona: seat.persona,
|
|
1371
|
+
status: signal.aborted ? "cancelled" : "failed",
|
|
1372
|
+
error: errorMessage(error)
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
const warnings = distinctnessWarnings(votes, profile);
|
|
1378
|
+
const errors = votes.filter((vote) => vote.status === "failed" || vote.status === "invalid").map((vote) => `${vote.seatId}: ${vote.error ?? vote.status}`);
|
|
1379
|
+
if (question.signal?.aborted) {
|
|
1380
|
+
return resultEnvelope({
|
|
1381
|
+
status: "cancelled",
|
|
1382
|
+
reason: "Council call cancelled.",
|
|
1383
|
+
resolution: "none",
|
|
1384
|
+
votes,
|
|
1385
|
+
profile,
|
|
1386
|
+
usage,
|
|
1387
|
+
startedAt,
|
|
1388
|
+
warnings,
|
|
1389
|
+
errors
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
if (timeoutSignal.aborted) {
|
|
1393
|
+
return resultEnvelope({
|
|
1394
|
+
status: "failed",
|
|
1395
|
+
reason: "Council overall timeout exceeded.",
|
|
1396
|
+
resolution: "none",
|
|
1397
|
+
votes,
|
|
1398
|
+
profile,
|
|
1399
|
+
usage,
|
|
1400
|
+
startedAt,
|
|
1401
|
+
warnings,
|
|
1402
|
+
errors: [...errors, "Council overall timeout exceeded."]
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
if (!question.options || question.options.length === 0) {
|
|
1406
|
+
return this.resolveOpenQuestion(
|
|
1407
|
+
question,
|
|
1408
|
+
profile,
|
|
1409
|
+
votes,
|
|
1410
|
+
signal,
|
|
1411
|
+
usage,
|
|
1412
|
+
startedAt,
|
|
1413
|
+
warnings,
|
|
1414
|
+
errors
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
return this.resolveOptionQuestion(
|
|
1418
|
+
question,
|
|
1419
|
+
profile,
|
|
1420
|
+
votes,
|
|
1421
|
+
signal,
|
|
1422
|
+
usage,
|
|
1423
|
+
startedAt,
|
|
1424
|
+
warnings,
|
|
1425
|
+
errors
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
async callSeat(question, profile, seat, signal, usage) {
|
|
1429
|
+
if (signal.aborted) return cancelledVote(seat);
|
|
1430
|
+
let persona;
|
|
1431
|
+
try {
|
|
1432
|
+
persona = this.personas.require(seat.persona);
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
return {
|
|
1435
|
+
seatId: seat.id,
|
|
1436
|
+
persona: seat.persona,
|
|
1437
|
+
status: "failed",
|
|
1438
|
+
error: errorMessage(error)
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const result = await this.safeCall({
|
|
1442
|
+
system: buildCouncilVoterSystemPrompt(persona),
|
|
1443
|
+
userPrompt: buildCouncilVoterUserPrompt(question, seat, {
|
|
1444
|
+
refusalOptionId: question.options?.length ? this.refusalOptionId : void 0
|
|
1445
|
+
}),
|
|
1446
|
+
target: seat.target,
|
|
1447
|
+
maxTokens: profile.voterMaxTokens,
|
|
1448
|
+
timeoutMs: profile.perCallTimeoutMs,
|
|
1449
|
+
signal,
|
|
1450
|
+
usage
|
|
1451
|
+
});
|
|
1452
|
+
const metadata = callMetadata(result);
|
|
1453
|
+
if (result.error) {
|
|
1454
|
+
return {
|
|
1455
|
+
seatId: seat.id,
|
|
1456
|
+
persona: seat.persona,
|
|
1457
|
+
status: signal.aborted ? "cancelled" : "failed",
|
|
1458
|
+
...metadata,
|
|
1459
|
+
error: result.error
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
const parsed = parseVote(result.text, question, this.refusalOptionId);
|
|
1463
|
+
if (!parsed.ok) {
|
|
1464
|
+
return {
|
|
1465
|
+
seatId: seat.id,
|
|
1466
|
+
persona: seat.persona,
|
|
1467
|
+
status: "invalid",
|
|
1468
|
+
...metadata,
|
|
1469
|
+
error: parsed.error
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
seatId: seat.id,
|
|
1474
|
+
persona: seat.persona,
|
|
1475
|
+
status: "valid",
|
|
1476
|
+
...parsed.vote,
|
|
1477
|
+
...metadata
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
async resolveOptionQuestion(question, profile, votes, signal, usage, startedAt, warnings, errors) {
|
|
1481
|
+
const validVotes = votes.filter(
|
|
1482
|
+
(vote) => vote.status === "valid" && typeof vote.optionId === "string"
|
|
1483
|
+
);
|
|
1484
|
+
const resolution = resolveCouncilVotes({
|
|
1485
|
+
seats: profile.seats.map((seat) => ({
|
|
1486
|
+
id: seat.id,
|
|
1487
|
+
weight: seat.weight,
|
|
1488
|
+
veto: seat.veto
|
|
1489
|
+
})),
|
|
1490
|
+
votes: validVotes.map((vote) => ({ seatId: vote.seatId, optionId: vote.optionId })),
|
|
1491
|
+
refusalOptionId: this.refusalOptionId,
|
|
1492
|
+
quorumFraction: profile.quorumFraction,
|
|
1493
|
+
approvalFraction: profile.approvalFraction
|
|
1494
|
+
});
|
|
1495
|
+
if (resolution.status === "abstained") {
|
|
1496
|
+
return resultEnvelope({
|
|
1497
|
+
status: "abstained",
|
|
1498
|
+
reason: "Council quorum was not met.",
|
|
1499
|
+
resolution: "none",
|
|
1500
|
+
votes,
|
|
1501
|
+
profile,
|
|
1502
|
+
usage,
|
|
1503
|
+
startedAt,
|
|
1504
|
+
warnings,
|
|
1505
|
+
errors
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
if (resolution.status === "denied") {
|
|
1509
|
+
return resultEnvelope({
|
|
1510
|
+
status: "denied",
|
|
1511
|
+
optionId: resolution.optionId,
|
|
1512
|
+
reason: `Council denied the proposal via ${resolution.method}.`,
|
|
1513
|
+
resolution: resolution.method,
|
|
1514
|
+
votes,
|
|
1515
|
+
profile,
|
|
1516
|
+
usage,
|
|
1517
|
+
startedAt,
|
|
1518
|
+
warnings,
|
|
1519
|
+
errors
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
if (resolution.status === "decided") {
|
|
1523
|
+
return resultEnvelope({
|
|
1524
|
+
status: "decided",
|
|
1525
|
+
optionId: resolution.optionId,
|
|
1526
|
+
answer: optionLabel(question, resolution.optionId),
|
|
1527
|
+
resolution: "majority",
|
|
1528
|
+
votes,
|
|
1529
|
+
profile,
|
|
1530
|
+
usage,
|
|
1531
|
+
startedAt,
|
|
1532
|
+
warnings,
|
|
1533
|
+
errors
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
if (!profile.judge) {
|
|
1537
|
+
return resultEnvelope({
|
|
1538
|
+
status: "abstained",
|
|
1539
|
+
reason: `Council requires a judge (${resolution.reason}), but this profile has none.`,
|
|
1540
|
+
resolution: "none",
|
|
1541
|
+
votes,
|
|
1542
|
+
profile,
|
|
1543
|
+
usage,
|
|
1544
|
+
startedAt,
|
|
1545
|
+
warnings,
|
|
1546
|
+
errors
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
const judged = await this.callJudge(
|
|
1550
|
+
question,
|
|
1551
|
+
profile,
|
|
1552
|
+
votes,
|
|
1553
|
+
profile.judge,
|
|
1554
|
+
resolution.reason,
|
|
1555
|
+
signal,
|
|
1556
|
+
usage
|
|
1557
|
+
);
|
|
1558
|
+
if (!judged.ok) {
|
|
1559
|
+
return resultEnvelope({
|
|
1560
|
+
status: signal.aborted ? "cancelled" : "abstained",
|
|
1561
|
+
reason: judged.error,
|
|
1562
|
+
resolution: "none",
|
|
1563
|
+
votes,
|
|
1564
|
+
profile,
|
|
1565
|
+
usage,
|
|
1566
|
+
startedAt,
|
|
1567
|
+
warnings,
|
|
1568
|
+
errors: [...errors, judged.error],
|
|
1569
|
+
judgeUsed: true
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
if (judged.value.optionId === this.refusalOptionId) {
|
|
1573
|
+
return resultEnvelope({
|
|
1574
|
+
status: "denied",
|
|
1575
|
+
optionId: this.refusalOptionId,
|
|
1576
|
+
reason: judged.value.rationale ?? "Council judge refused all options.",
|
|
1577
|
+
resolution: "judge",
|
|
1578
|
+
votes,
|
|
1579
|
+
profile,
|
|
1580
|
+
usage,
|
|
1581
|
+
startedAt,
|
|
1582
|
+
warnings,
|
|
1583
|
+
errors,
|
|
1584
|
+
judgeUsed: true
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
return resultEnvelope({
|
|
1588
|
+
status: "decided",
|
|
1589
|
+
optionId: judged.value.optionId,
|
|
1590
|
+
answer: optionLabel(question, judged.value.optionId),
|
|
1591
|
+
reason: judged.value.rationale,
|
|
1592
|
+
resolution: "judge",
|
|
1593
|
+
votes,
|
|
1594
|
+
profile,
|
|
1595
|
+
usage,
|
|
1596
|
+
startedAt,
|
|
1597
|
+
warnings,
|
|
1598
|
+
errors,
|
|
1599
|
+
judgeUsed: true
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
async resolveOpenQuestion(question, profile, votes, signal, usage, startedAt, warnings, errors) {
|
|
1603
|
+
const valid = votes.filter(
|
|
1604
|
+
(vote) => vote.status === "valid" && typeof vote.stance === "string"
|
|
1605
|
+
);
|
|
1606
|
+
if (valid.length / profile.seats.length < profile.quorumFraction) {
|
|
1607
|
+
return resultEnvelope({
|
|
1608
|
+
status: "abstained",
|
|
1609
|
+
reason: "Council quorum was not met.",
|
|
1610
|
+
resolution: "none",
|
|
1611
|
+
votes,
|
|
1612
|
+
profile,
|
|
1613
|
+
usage,
|
|
1614
|
+
startedAt,
|
|
1615
|
+
warnings,
|
|
1616
|
+
errors
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
if (!profile.judge) {
|
|
1620
|
+
const first = valid[0];
|
|
1621
|
+
if (!first) {
|
|
1622
|
+
return resultEnvelope({
|
|
1623
|
+
status: "failed",
|
|
1624
|
+
reason: "Council produced no valid stance.",
|
|
1625
|
+
resolution: "none",
|
|
1626
|
+
votes,
|
|
1627
|
+
profile,
|
|
1628
|
+
usage,
|
|
1629
|
+
startedAt,
|
|
1630
|
+
warnings,
|
|
1631
|
+
errors
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
return resultEnvelope({
|
|
1635
|
+
status: "decided",
|
|
1636
|
+
answer: first.stance,
|
|
1637
|
+
reason: first.rationale,
|
|
1638
|
+
resolution: "first_stance",
|
|
1639
|
+
votes,
|
|
1640
|
+
profile,
|
|
1641
|
+
usage,
|
|
1642
|
+
startedAt,
|
|
1643
|
+
warnings,
|
|
1644
|
+
errors
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
const judged = await this.callJudge(
|
|
1648
|
+
question,
|
|
1649
|
+
profile,
|
|
1650
|
+
votes,
|
|
1651
|
+
profile.judge,
|
|
1652
|
+
"open_question_synthesis",
|
|
1653
|
+
signal,
|
|
1654
|
+
usage
|
|
1655
|
+
);
|
|
1656
|
+
if (!judged.ok) {
|
|
1657
|
+
return resultEnvelope({
|
|
1658
|
+
status: signal.aborted ? "cancelled" : "failed",
|
|
1659
|
+
reason: judged.error,
|
|
1660
|
+
resolution: "none",
|
|
1661
|
+
votes,
|
|
1662
|
+
profile,
|
|
1663
|
+
usage,
|
|
1664
|
+
startedAt,
|
|
1665
|
+
warnings,
|
|
1666
|
+
errors: [...errors, judged.error],
|
|
1667
|
+
judgeUsed: true
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
return resultEnvelope({
|
|
1671
|
+
status: "decided",
|
|
1672
|
+
answer: judged.value.answer,
|
|
1673
|
+
reason: judged.value.rationale,
|
|
1674
|
+
resolution: "judge",
|
|
1675
|
+
votes,
|
|
1676
|
+
profile,
|
|
1677
|
+
usage,
|
|
1678
|
+
startedAt,
|
|
1679
|
+
warnings,
|
|
1680
|
+
errors,
|
|
1681
|
+
judgeUsed: true
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
async callJudge(question, profile, votes, target, reason, signal, usage) {
|
|
1685
|
+
const result = await this.safeCall({
|
|
1686
|
+
system: buildCouncilJudgeSystemPrompt(),
|
|
1687
|
+
userPrompt: buildCouncilJudgeUserPrompt(question, votes, {
|
|
1688
|
+
reason,
|
|
1689
|
+
refusalOptionId: question.options?.length ? this.refusalOptionId : void 0
|
|
1690
|
+
}),
|
|
1691
|
+
target,
|
|
1692
|
+
maxTokens: profile.judgeMaxTokens,
|
|
1693
|
+
timeoutMs: profile.perCallTimeoutMs,
|
|
1694
|
+
signal,
|
|
1695
|
+
usage
|
|
1696
|
+
});
|
|
1697
|
+
if (result.error) return { ok: false, error: result.error };
|
|
1698
|
+
return parseJudge(result.text, question, this.refusalOptionId);
|
|
1699
|
+
}
|
|
1700
|
+
async safeCall(input) {
|
|
1701
|
+
try {
|
|
1702
|
+
const result = await this.caller.call({
|
|
1703
|
+
system: input.system,
|
|
1704
|
+
userPrompt: input.userPrompt,
|
|
1705
|
+
responseFormat: { type: "json_object" },
|
|
1706
|
+
maxTokens: input.maxTokens,
|
|
1707
|
+
timeoutMs: input.timeoutMs,
|
|
1708
|
+
signal: input.signal,
|
|
1709
|
+
...input.target?.providerId ? { providerId: input.target.providerId } : {},
|
|
1710
|
+
...input.target?.model ? { model: input.target.model } : {},
|
|
1711
|
+
...input.target?.role ? { role: input.target.role } : {},
|
|
1712
|
+
...input.target?.fallbackProfile ? { fallbackProfile: input.target.fallbackProfile } : {},
|
|
1713
|
+
...input.target?.fallbackModels ? { fallbackModels: [...input.target.fallbackModels] } : {}
|
|
1714
|
+
});
|
|
1715
|
+
addUsage(input.usage, result);
|
|
1716
|
+
return result;
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
input.usage.calls += 1;
|
|
1719
|
+
return emptyCallResult(errorMessage(error));
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
function parseVote(text, question, refusalOptionId) {
|
|
1724
|
+
const parsed = parseObject(text);
|
|
1725
|
+
if (!parsed.ok) return parsed;
|
|
1726
|
+
const rationale = optionalString(parsed.value["rationale"]);
|
|
1727
|
+
if (question.options && question.options.length > 0) {
|
|
1728
|
+
const optionId = optionalString(parsed.value["optionId"]);
|
|
1729
|
+
const allowed = /* @__PURE__ */ new Set([...question.options.map((option) => option.id.trim()), refusalOptionId]);
|
|
1730
|
+
if (!optionId || !allowed.has(optionId)) {
|
|
1731
|
+
return { ok: false, error: "Voter returned an unknown or missing optionId." };
|
|
1732
|
+
}
|
|
1733
|
+
return { ok: true, vote: { optionId, ...rationale ? { rationale } : {} } };
|
|
1734
|
+
}
|
|
1735
|
+
const stance = optionalString(parsed.value["stance"]);
|
|
1736
|
+
if (!stance) return { ok: false, error: "Voter returned an empty or missing stance." };
|
|
1737
|
+
return { ok: true, vote: { stance, ...rationale ? { rationale } : {} } };
|
|
1738
|
+
}
|
|
1739
|
+
function parseJudge(text, question, refusalOptionId) {
|
|
1740
|
+
const parsed = parseObject(text);
|
|
1741
|
+
if (!parsed.ok) return parsed;
|
|
1742
|
+
const rationale = optionalString(parsed.value["rationale"]);
|
|
1743
|
+
if (question.options && question.options.length > 0) {
|
|
1744
|
+
const optionId = optionalString(parsed.value["optionId"]);
|
|
1745
|
+
const allowed = /* @__PURE__ */ new Set([...question.options.map((option) => option.id.trim()), refusalOptionId]);
|
|
1746
|
+
if (!optionId || !allowed.has(optionId)) {
|
|
1747
|
+
return { ok: false, error: "Judge returned an unknown or missing optionId." };
|
|
1748
|
+
}
|
|
1749
|
+
return { ok: true, value: { optionId, ...rationale ? { rationale } : {} } };
|
|
1750
|
+
}
|
|
1751
|
+
const answer = optionalString(parsed.value["answer"]);
|
|
1752
|
+
if (!answer) return { ok: false, error: "Judge returned an empty or missing answer." };
|
|
1753
|
+
return { ok: true, value: { answer, ...rationale ? { rationale } : {} } };
|
|
1754
|
+
}
|
|
1755
|
+
function parseObject(text) {
|
|
1756
|
+
const trimmed = text.trim();
|
|
1757
|
+
const first = trimmed.indexOf("{");
|
|
1758
|
+
const last = trimmed.lastIndexOf("}");
|
|
1759
|
+
if (first < 0 || last < first) return { ok: false, error: "LLM response did not contain JSON." };
|
|
1760
|
+
try {
|
|
1761
|
+
const value = JSON.parse(trimmed.slice(first, last + 1));
|
|
1762
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1763
|
+
return { ok: false, error: "LLM response JSON must be an object." };
|
|
1764
|
+
}
|
|
1765
|
+
return { ok: true, value };
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
return { ok: false, error: `Invalid LLM response JSON: ${errorMessage(error)}` };
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function resultEnvelope(input) {
|
|
1771
|
+
const validVoteCount = input.votes.filter((vote) => vote.status === "valid").length;
|
|
1772
|
+
return {
|
|
1773
|
+
status: input.status,
|
|
1774
|
+
...input.answer ? { answer: input.answer } : {},
|
|
1775
|
+
...input.optionId ? { optionId: input.optionId } : {},
|
|
1776
|
+
...input.reason ? { reason: input.reason } : {},
|
|
1777
|
+
resolution: input.resolution,
|
|
1778
|
+
votes: Object.freeze([...input.votes]),
|
|
1779
|
+
configuredSeatCount: input.profile.seats.length,
|
|
1780
|
+
validVoteCount,
|
|
1781
|
+
distinctTargetCount: distinctTargetCount(input.votes, input.profile),
|
|
1782
|
+
judgeUsed: input.judgeUsed ?? false,
|
|
1783
|
+
usage: usageResult(input.usage, input.startedAt),
|
|
1784
|
+
...input.warnings.length > 0 ? { warnings: Object.freeze([...input.warnings]) } : {},
|
|
1785
|
+
...input.errors.length > 0 ? { errors: Object.freeze([...input.errors]) } : {}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function callMetadata(result) {
|
|
1789
|
+
return {
|
|
1790
|
+
...result.provider ? { provider: result.provider } : {},
|
|
1791
|
+
...result.model ? { model: result.model } : {},
|
|
1792
|
+
...result.fromFallback ? { fromFallback: true } : {},
|
|
1793
|
+
durationMs: result.durationMs
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
function addUsage(usage, result) {
|
|
1797
|
+
usage.calls += 1;
|
|
1798
|
+
usage.inputTokens += result.tokens.input;
|
|
1799
|
+
usage.outputTokens += result.tokens.output;
|
|
1800
|
+
usage.totalTokens += result.tokens.total;
|
|
1801
|
+
}
|
|
1802
|
+
function usageResult(usage, startedAt) {
|
|
1803
|
+
return Object.freeze({ ...usage, durationMs: Math.max(0, Date.now() - startedAt) });
|
|
1804
|
+
}
|
|
1805
|
+
function cancelledVote(seat) {
|
|
1806
|
+
return { seatId: seat.id, persona: seat.persona, status: "cancelled", error: "Cancelled." };
|
|
1807
|
+
}
|
|
1808
|
+
function distinctTargetCount(votes, profile) {
|
|
1809
|
+
const keys = votes.filter((vote) => vote.status === "valid").map(
|
|
1810
|
+
(vote) => profile.distinctness === "provider" ? vote.provider : `${vote.provider ?? ""}/${vote.model ?? ""}`
|
|
1811
|
+
).filter(Boolean);
|
|
1812
|
+
return new Set(keys).size;
|
|
1813
|
+
}
|
|
1814
|
+
function distinctnessWarnings(votes, profile) {
|
|
1815
|
+
if (profile.distinctness === "none") return [];
|
|
1816
|
+
const valid = votes.filter((vote) => vote.status === "valid");
|
|
1817
|
+
const distinct = distinctTargetCount(valid, profile);
|
|
1818
|
+
if (valid.length > 1 && distinct < valid.length) {
|
|
1819
|
+
return [
|
|
1820
|
+
`Council distinctness policy "${profile.distinctness}" was not met: ${distinct} distinct target(s) served ${valid.length} valid vote(s).`
|
|
1821
|
+
];
|
|
1822
|
+
}
|
|
1823
|
+
return [];
|
|
1824
|
+
}
|
|
1825
|
+
function optionLabel(question, optionId) {
|
|
1826
|
+
if (!optionId) return void 0;
|
|
1827
|
+
return question.options?.find((option) => option.id.trim() === optionId)?.label.trim();
|
|
1828
|
+
}
|
|
1829
|
+
function validateRefusalCollision(question, refusalOptionId) {
|
|
1830
|
+
if (question.options?.some((option) => option.id.trim() === refusalOptionId)) {
|
|
1831
|
+
throw new Error(`CouncilOrchestrator: option id "${refusalOptionId}" is reserved.`);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
function validateConcurrency(value) {
|
|
1835
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_COUNCIL_CONCURRENCY) {
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
`CouncilOrchestrator: maxConcurrency must be an integer in [1, ${MAX_COUNCIL_CONCURRENCY}].`
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
return value;
|
|
1841
|
+
}
|
|
1842
|
+
async function mapConcurrent(items, concurrency, worker) {
|
|
1843
|
+
const results = new Array(items.length);
|
|
1844
|
+
let next = 0;
|
|
1845
|
+
const run = async () => {
|
|
1846
|
+
while (true) {
|
|
1847
|
+
const index = next++;
|
|
1848
|
+
if (index >= items.length) return;
|
|
1849
|
+
const item = items[index];
|
|
1850
|
+
if (item !== void 0) results[index] = await worker(item, index);
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => run()));
|
|
1854
|
+
return results;
|
|
1855
|
+
}
|
|
1856
|
+
function emptyCallResult(error) {
|
|
1857
|
+
return {
|
|
1858
|
+
text: "",
|
|
1859
|
+
model: "",
|
|
1860
|
+
provider: "",
|
|
1861
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
1862
|
+
durationMs: 0,
|
|
1863
|
+
fromFallback: false,
|
|
1864
|
+
error
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
function optionalString(value) {
|
|
1868
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1869
|
+
}
|
|
1870
|
+
function errorMessage(error) {
|
|
1871
|
+
return error instanceof Error ? error.message : String(error);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// src/tools/council-tool.ts
|
|
1875
|
+
var COUNCIL_TOOL_NAME = "council";
|
|
1876
|
+
var MAX_COUNCIL_TOOL_OPTIONS = 12;
|
|
1877
|
+
var MAX_COUNCIL_QUESTION_CHARS = 2e4;
|
|
1878
|
+
var MAX_COUNCIL_CONTEXT_CHARS = 8e4;
|
|
1879
|
+
var INPUT_SCHEMA = {
|
|
1880
|
+
type: "object",
|
|
1881
|
+
properties: {
|
|
1882
|
+
question: {
|
|
1883
|
+
type: "string",
|
|
1884
|
+
description: "The decision or open question for the Council.",
|
|
1885
|
+
maxLength: MAX_COUNCIL_QUESTION_CHARS
|
|
1886
|
+
},
|
|
1887
|
+
context: {
|
|
1888
|
+
type: "string",
|
|
1889
|
+
description: "Optional evidence and constraints. Treated as untrusted quoted data.",
|
|
1890
|
+
maxLength: MAX_COUNCIL_CONTEXT_CHARS
|
|
1891
|
+
},
|
|
1892
|
+
options: {
|
|
1893
|
+
type: "array",
|
|
1894
|
+
maxItems: MAX_COUNCIL_TOOL_OPTIONS,
|
|
1895
|
+
items: {
|
|
1896
|
+
type: "object",
|
|
1897
|
+
properties: {
|
|
1898
|
+
id: { type: "string", description: "Stable option id." },
|
|
1899
|
+
label: { type: "string", description: "Human-readable option label." },
|
|
1900
|
+
consequence: { type: "string", description: "Optional consequence or trade-off." }
|
|
1901
|
+
},
|
|
1902
|
+
required: ["id", "label"],
|
|
1903
|
+
additionalProperties: false
|
|
1904
|
+
},
|
|
1905
|
+
description: "Optional bounded list of choices. Omit for an open-ended Council answer."
|
|
1906
|
+
},
|
|
1907
|
+
profile: {
|
|
1908
|
+
type: "string",
|
|
1909
|
+
description: "Registered Council profile id. Defaults to the host-configured profile."
|
|
1910
|
+
}
|
|
1911
|
+
},
|
|
1912
|
+
required: ["question"],
|
|
1913
|
+
additionalProperties: false
|
|
1914
|
+
};
|
|
1915
|
+
function createCouncilTool(opts) {
|
|
1916
|
+
const orchestrator = new CouncilOrchestrator(opts);
|
|
1917
|
+
return {
|
|
1918
|
+
name: COUNCIL_TOOL_NAME,
|
|
1919
|
+
description: "Ask an independent, multi-persona Council to evaluate a decision or synthesize an answer. Uses bounded parallel voters, quorum/veto/weighted resolution, optional judging, model routing, fallback chains, and cancellation.",
|
|
1920
|
+
usageHint: "Use for consequential or disputed decisions that benefit from independent lenses. Provide `options` for a vote or omit them for an open answer. Keep context evidence-focused; the Council treats it as untrusted data.",
|
|
1921
|
+
category: "meta",
|
|
1922
|
+
inputSchema: INPUT_SCHEMA,
|
|
1923
|
+
permission: "auto",
|
|
1924
|
+
mutating: false,
|
|
1925
|
+
riskTier: "safe",
|
|
1926
|
+
managesOwnTimeout: true,
|
|
1927
|
+
maxOutputBytes: 256e3,
|
|
1928
|
+
async execute(input, _ctx, { signal }) {
|
|
1929
|
+
const question = {
|
|
1930
|
+
question: input.question,
|
|
1931
|
+
...input.context ? { context: input.context } : {},
|
|
1932
|
+
...input.options ? { options: input.options } : {},
|
|
1933
|
+
...input.profile ? { profile: input.profile } : {},
|
|
1934
|
+
signal
|
|
1935
|
+
};
|
|
1936
|
+
return orchestrator.ask(question);
|
|
1937
|
+
},
|
|
1938
|
+
validate: validateCouncilToolInput
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
function validateCouncilToolInput(input) {
|
|
1942
|
+
const errors = [];
|
|
1943
|
+
const question = input.question?.trim() ?? "";
|
|
1944
|
+
if (!question) errors.push("`question` must not be empty.");
|
|
1945
|
+
if (question.length > MAX_COUNCIL_QUESTION_CHARS) {
|
|
1946
|
+
errors.push(`\`question\` must not exceed ${MAX_COUNCIL_QUESTION_CHARS} characters.`);
|
|
1947
|
+
}
|
|
1948
|
+
if ((input.context?.length ?? 0) > MAX_COUNCIL_CONTEXT_CHARS) {
|
|
1949
|
+
errors.push(`\`context\` must not exceed ${MAX_COUNCIL_CONTEXT_CHARS} characters.`);
|
|
1950
|
+
}
|
|
1951
|
+
if ((input.options?.length ?? 0) > MAX_COUNCIL_TOOL_OPTIONS) {
|
|
1952
|
+
errors.push(`\`options\` must not contain more than ${MAX_COUNCIL_TOOL_OPTIONS} items.`);
|
|
1953
|
+
}
|
|
1954
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1955
|
+
for (const option of input.options ?? []) {
|
|
1956
|
+
const id = option.id.trim();
|
|
1957
|
+
if (!id) errors.push("Every option must have a non-empty `id`.");
|
|
1958
|
+
if (!option.label.trim()) errors.push(`Option "${id || "<empty>"}" must have a label.`);
|
|
1959
|
+
if (ids.has(id)) errors.push(`Duplicate option id "${id}".`);
|
|
1960
|
+
ids.add(id);
|
|
1961
|
+
}
|
|
1962
|
+
return errors;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// src/types/blocks.ts
|
|
1966
|
+
function isTextBlock(b) {
|
|
1967
|
+
return b.type === "text";
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/types/provider.ts
|
|
1971
|
+
var CONTEXT_OVERFLOW_RE = /context.length|context.window|max.*tokens?.*exceeded|prompt is too long|too long|exceeds the context|\btokens\b.*exceed|context_length_exceeded/i;
|
|
1972
|
+
var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
|
|
1973
|
+
function classifyProviderError(status, body, message) {
|
|
1974
|
+
const type = body?.type;
|
|
1975
|
+
if (status === 0) return "network";
|
|
1976
|
+
if (status === 408) return "timeout";
|
|
1977
|
+
if (status === 599) return "stream_hang";
|
|
1978
|
+
if (type === "rate_limit_error" || status === 429) return "rate_limit";
|
|
1979
|
+
if (type === "overloaded_error" || status === 529) return "overloaded";
|
|
1980
|
+
if (status >= 500) return "server";
|
|
1981
|
+
if (type === "authentication_error" || type === "permission_error" || status === 401 || status === 403) {
|
|
1982
|
+
return "auth";
|
|
1983
|
+
}
|
|
1984
|
+
const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
|
|
1985
|
+
if (type === "content_filter" || CONTENT_FILTER_RE.test(text)) return "content_filter";
|
|
1986
|
+
if (status === 413 || status >= 400 && CONTEXT_OVERFLOW_RE.test(text)) {
|
|
1987
|
+
return "context_overflow";
|
|
1988
|
+
}
|
|
1989
|
+
if (status >= 400) return "invalid_request";
|
|
1990
|
+
return "unknown";
|
|
1991
|
+
}
|
|
1992
|
+
function isFallbackWorthy(kind) {
|
|
1993
|
+
return FALLBACK_WORTHY_BY_KIND[kind];
|
|
1994
|
+
}
|
|
1995
|
+
var FALLBACK_WORTHY_BY_KIND = {
|
|
1996
|
+
rate_limit: true,
|
|
1997
|
+
overloaded: true,
|
|
1998
|
+
server: true,
|
|
1999
|
+
timeout: true,
|
|
2000
|
+
network: true,
|
|
2001
|
+
stream_hang: true,
|
|
2002
|
+
auth: false,
|
|
2003
|
+
context_overflow: false,
|
|
2004
|
+
content_filter: false,
|
|
2005
|
+
invalid_request: false,
|
|
2006
|
+
unknown: false
|
|
2007
|
+
};
|
|
2008
|
+
var ProviderError = class extends WrongStackError {
|
|
2009
|
+
status;
|
|
2010
|
+
retryable;
|
|
2011
|
+
providerId;
|
|
2012
|
+
/** Canonical failure classification — see {@link ProviderErrorKind}. */
|
|
2013
|
+
kind;
|
|
2014
|
+
body;
|
|
2015
|
+
constructor(message, status, retryable, providerId, opts = {}) {
|
|
2016
|
+
const kind = opts.kind ?? classifyProviderError(status, opts.body, message);
|
|
2017
|
+
super({
|
|
2018
|
+
message,
|
|
2019
|
+
code: kindToCode(kind),
|
|
2020
|
+
subsystem: "provider",
|
|
2021
|
+
severity: status >= 500 ? "error" : "warning",
|
|
2022
|
+
recoverable: retryable,
|
|
2023
|
+
context: { providerId, status },
|
|
2024
|
+
cause: opts.cause
|
|
2025
|
+
});
|
|
2026
|
+
this.name = "ProviderError";
|
|
2027
|
+
this.status = status;
|
|
2028
|
+
this.retryable = retryable;
|
|
2029
|
+
this.providerId = providerId;
|
|
2030
|
+
this.kind = kind;
|
|
2031
|
+
this.body = opts.body;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Render a one-line, user-facing description. Designed for the CLI/TUI
|
|
2035
|
+
* status line and the agent's retry warning. Avoids dumping raw JSON
|
|
2036
|
+
* (which is what users see today when a 529 lands and the log message
|
|
2037
|
+
* includes the full `{"type":"error",...}` body).
|
|
2038
|
+
*
|
|
2039
|
+
* Examples:
|
|
2040
|
+
* "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
|
|
2041
|
+
* "openai rate limited (429): Retry after 12s"
|
|
2042
|
+
* "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
|
|
2043
|
+
* "groq HTTP 500 (server error)"
|
|
2044
|
+
*/
|
|
2045
|
+
describe() {
|
|
2046
|
+
const kind = describeStatus(this.status, this.body?.type);
|
|
2047
|
+
const head = `${this.providerId} ${kind}`;
|
|
2048
|
+
const detail = this.body?.message?.trim();
|
|
2049
|
+
const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
|
|
2050
|
+
if (detail && detail.length > 0) {
|
|
2051
|
+
return `${head}: ${truncate(detail, 240)}${reqId}`;
|
|
2052
|
+
}
|
|
2053
|
+
return `${head}${reqId}`;
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
function describeStatus(status, type) {
|
|
2057
|
+
if (status === 0) return "network error";
|
|
2058
|
+
if (status === 599) return `stream hang (${status})`;
|
|
2059
|
+
if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
|
|
2060
|
+
if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
|
|
2061
|
+
if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
|
|
2062
|
+
if (type === "permission_error" || status === 403) return `forbidden (${status})`;
|
|
2063
|
+
if (type === "not_found_error" || status === 404) return `not found (${status})`;
|
|
2064
|
+
if (type === "content_filter") return `content filtered (${status})`;
|
|
2065
|
+
if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
|
|
2066
|
+
if (status === 408) return `timeout (${status})`;
|
|
2067
|
+
if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
|
|
2068
|
+
if (type) return `${type} (${status})`;
|
|
2069
|
+
return `HTTP ${status}`;
|
|
2070
|
+
}
|
|
2071
|
+
var KIND_TO_CODE = {
|
|
2072
|
+
network: ERROR_CODES.PROVIDER_NETWORK_ERROR,
|
|
2073
|
+
timeout: ERROR_CODES.PROVIDER_NETWORK_ERROR,
|
|
2074
|
+
rate_limit: ERROR_CODES.PROVIDER_RATE_LIMITED,
|
|
2075
|
+
auth: ERROR_CODES.PROVIDER_AUTH_FAILED,
|
|
2076
|
+
overloaded: ERROR_CODES.PROVIDER_OVERLOADED,
|
|
2077
|
+
context_overflow: ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW,
|
|
2078
|
+
server: ERROR_CODES.PROVIDER_SERVER_ERROR,
|
|
2079
|
+
stream_hang: ERROR_CODES.PROVIDER_SERVER_ERROR,
|
|
2080
|
+
content_filter: ERROR_CODES.PROVIDER_INVALID_REQUEST,
|
|
2081
|
+
invalid_request: ERROR_CODES.PROVIDER_INVALID_REQUEST,
|
|
2082
|
+
unknown: ERROR_CODES.PROVIDER_INVALID_REQUEST
|
|
2083
|
+
};
|
|
2084
|
+
function kindToCode(kind) {
|
|
2085
|
+
return KIND_TO_CODE[kind];
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/core/fallback-model.ts
|
|
2089
|
+
function visibleProviderModels(config, providerId, providerModels) {
|
|
2090
|
+
const entry = config.providers?.[providerId];
|
|
2091
|
+
return entry?.models !== void 0 ? [...entry.models] : providerModels;
|
|
2092
|
+
}
|
|
2093
|
+
function parseModelRef(ref) {
|
|
2094
|
+
const trimmed = ref.trim();
|
|
2095
|
+
const slash = trimmed.indexOf("/");
|
|
2096
|
+
if (slash !== -1) {
|
|
2097
|
+
return {
|
|
2098
|
+
provider: trimmed.slice(0, slash) || void 0,
|
|
2099
|
+
model: trimmed.slice(slash + 1).trim()
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
const parts = trimmed.split(/\s+/);
|
|
2103
|
+
if (parts.length >= 2) {
|
|
2104
|
+
return { provider: parts[0], model: parts.slice(1).join(" ") };
|
|
2105
|
+
}
|
|
2106
|
+
return { model: trimmed };
|
|
2107
|
+
}
|
|
2108
|
+
function formatModelRef(ref, defaultProvider) {
|
|
2109
|
+
const provider = ref.provider ?? defaultProvider;
|
|
2110
|
+
return provider ? `${provider}/${ref.model}` : ref.model;
|
|
2111
|
+
}
|
|
2112
|
+
function normalizeModelRef(ref, defaultProvider) {
|
|
2113
|
+
const parsed = parseModelRef(ref);
|
|
2114
|
+
return formatModelRef(parsed, defaultProvider);
|
|
2115
|
+
}
|
|
2116
|
+
function fallbackProfileChain(config, profileName) {
|
|
2117
|
+
if (!profileName) return [];
|
|
2118
|
+
const chain = config.fallbackProfiles?.[profileName];
|
|
2119
|
+
return Array.isArray(chain) ? chain.filter((ref) => parseModelRef(ref).model) : [];
|
|
2120
|
+
}
|
|
2121
|
+
function providerHasKey(entry) {
|
|
2122
|
+
if (!entry) return false;
|
|
2123
|
+
if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
|
|
2124
|
+
if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => k?.apiKey)) return true;
|
|
2125
|
+
if (Array.isArray(entry.envVars) && entry.envVars.some((v) => !!process.env[v])) return true;
|
|
2126
|
+
return false;
|
|
2127
|
+
}
|
|
2128
|
+
var SMART_DEFAULT_MAX = 4;
|
|
2129
|
+
function smartDefaultFallbackChain(config) {
|
|
2130
|
+
const leaderProvider = config.provider;
|
|
2131
|
+
const leaderModel = config.model;
|
|
2132
|
+
const providers = config.providers ?? {};
|
|
2133
|
+
const favoriteSet = new Set(
|
|
2134
|
+
(config.favoriteModels ?? []).map((ref) => normalizeModelRef(ref, leaderProvider))
|
|
2135
|
+
);
|
|
2136
|
+
const hasFavorites = favoriteSet.size > 0;
|
|
2137
|
+
const favoritesOnly = config.favoriteModelsOnly === true;
|
|
2138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2139
|
+
const favoriteRefs = [];
|
|
2140
|
+
const sameProvider = [];
|
|
2141
|
+
const crossProvider = [];
|
|
2142
|
+
const ids = Object.keys(providers).sort(
|
|
2143
|
+
(a, b) => a === leaderProvider ? -1 : b === leaderProvider ? 1 : a.localeCompare(b)
|
|
2144
|
+
);
|
|
2145
|
+
for (const id of ids) {
|
|
2146
|
+
const entry = providers[id];
|
|
2147
|
+
if (!providerHasKey(entry)) continue;
|
|
2148
|
+
const models = visibleProviderModels(config, id, entry?.models ?? []);
|
|
2149
|
+
for (const model of models) {
|
|
2150
|
+
if (id === leaderProvider && model === leaderModel) continue;
|
|
2151
|
+
const ref = `${id}/${model}`;
|
|
2152
|
+
if (seen.has(ref)) continue;
|
|
2153
|
+
seen.add(ref);
|
|
2154
|
+
if (favoriteSet.has(ref)) {
|
|
2155
|
+
favoriteRefs.push(ref);
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
if (favoritesOnly && hasFavorites) continue;
|
|
2159
|
+
(id === leaderProvider ? sameProvider : crossProvider).push(ref);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return [...favoriteRefs, ...sameProvider, ...crossProvider].slice(0, SMART_DEFAULT_MAX);
|
|
2163
|
+
}
|
|
2164
|
+
function effectiveFallbackChain(config) {
|
|
2165
|
+
const explicit = config.fallbackModels ?? [];
|
|
2166
|
+
const filteredExplicit = explicit.filter((ref) => {
|
|
2167
|
+
const parsed = parseModelRef(ref);
|
|
2168
|
+
if (!parsed.model) return false;
|
|
2169
|
+
const providerId = parsed.provider ?? config.provider;
|
|
2170
|
+
const entry = config.providers?.[providerId];
|
|
2171
|
+
if (!entry?.models) return true;
|
|
2172
|
+
return entry.models.includes(parsed.model);
|
|
2173
|
+
});
|
|
2174
|
+
if (filteredExplicit.length > 0) return filteredExplicit;
|
|
2175
|
+
if (config.fallbackAuto === false) return [];
|
|
2176
|
+
return smartDefaultFallbackChain(config);
|
|
2177
|
+
}
|
|
2178
|
+
var DEFAULT_PRIMARY_COOLDOWN_MAX_MS = 10 * 6e4;
|
|
2179
|
+
|
|
2180
|
+
// src/execution/one-shot-llm.ts
|
|
2181
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
2182
|
+
var DEFAULT_MAX_TOKENS = 1024;
|
|
2183
|
+
var OneShotOrchestrator = class {
|
|
2184
|
+
opts;
|
|
2185
|
+
constructor(opts) {
|
|
2186
|
+
this.opts = opts;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Make a one-shot LLM call. Resolves provider+model, applies fallback
|
|
2190
|
+
* chain on transient errors, and returns a structured result.
|
|
2191
|
+
*
|
|
2192
|
+
* Never throws — all errors are captured in `OneShotLLMResult.error`.
|
|
2193
|
+
*/
|
|
2194
|
+
async call(input) {
|
|
2195
|
+
const startedAt = performance.now();
|
|
2196
|
+
const config = this.opts.getConfig();
|
|
2197
|
+
const target = this.resolveTarget(input, config);
|
|
2198
|
+
if (!target) {
|
|
2199
|
+
return {
|
|
2200
|
+
text: "",
|
|
2201
|
+
model: input.model ?? config.model ?? "unknown",
|
|
2202
|
+
provider: input.providerId ?? config.provider ?? "unknown",
|
|
2203
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
2204
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2205
|
+
fromFallback: false,
|
|
2206
|
+
error: "No provider or model could be resolved. Check your config."
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
let provider;
|
|
2210
|
+
try {
|
|
2211
|
+
provider = await this.opts.buildProvider(target.providerId, target.model);
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
return {
|
|
2214
|
+
text: "",
|
|
2215
|
+
model: target.model,
|
|
2216
|
+
provider: target.providerId,
|
|
2217
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
2218
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2219
|
+
fromFallback: false,
|
|
2220
|
+
error: `Cannot build provider "${target.providerId}": ${err instanceof Error ? err.message : String(err)}`
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
const request = this.buildRequest(input, target.model);
|
|
2224
|
+
const signal = this.resolveSignal(input);
|
|
2225
|
+
const chain = this.resolveFallbackChain(input, config, target);
|
|
2226
|
+
let servingProviderId = provider.id;
|
|
2227
|
+
let servingModel = target.model;
|
|
2228
|
+
let fromFallback = false;
|
|
2229
|
+
const primaryAttempt = await this.tryCall(provider, request, signal);
|
|
2230
|
+
let result = primaryAttempt.response;
|
|
2231
|
+
let lastError = primaryAttempt.error;
|
|
2232
|
+
let fallbackEligible = primaryAttempt.fallbackEligible;
|
|
2233
|
+
if (!result && fallbackEligible && chain.length > 0) {
|
|
2234
|
+
for (const ref of chain) {
|
|
2235
|
+
const parsed = parseModelRef(ref);
|
|
2236
|
+
if (!parsed.model) continue;
|
|
2237
|
+
const fbProviderId = parsed.provider ?? config.provider;
|
|
2238
|
+
if (fbProviderId === provider.id && parsed.model === target.model) continue;
|
|
2239
|
+
servingProviderId = fbProviderId;
|
|
2240
|
+
servingModel = parsed.model;
|
|
2241
|
+
let fbProvider;
|
|
2242
|
+
try {
|
|
2243
|
+
fbProvider = await this.opts.buildProvider(fbProviderId, parsed.model);
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
lastError = err;
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
servingProviderId = fbProvider.id;
|
|
2249
|
+
const attempt = await this.tryCall(
|
|
2250
|
+
fbProvider,
|
|
2251
|
+
this.buildRequest(input, parsed.model),
|
|
2252
|
+
signal
|
|
2253
|
+
);
|
|
2254
|
+
if (attempt.response) {
|
|
2255
|
+
result = attempt.response;
|
|
2256
|
+
fromFallback = true;
|
|
2257
|
+
break;
|
|
2258
|
+
}
|
|
2259
|
+
lastError = attempt.error;
|
|
2260
|
+
fallbackEligible = attempt.fallbackEligible;
|
|
2261
|
+
if (!fallbackEligible) break;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
const elapsed = Math.round(performance.now() - startedAt);
|
|
2265
|
+
if (result) {
|
|
2266
|
+
const textBlocks = result.content.filter(isTextBlock);
|
|
2267
|
+
const text = textBlocks.map((b) => b.text).join("\n").trim();
|
|
2268
|
+
return {
|
|
2269
|
+
text: text || "(empty response)",
|
|
2270
|
+
model: result.model ?? servingModel,
|
|
2271
|
+
provider: servingProviderId,
|
|
2272
|
+
tokens: {
|
|
2273
|
+
input: result.usage?.input ?? 0,
|
|
2274
|
+
output: result.usage?.output ?? 0,
|
|
2275
|
+
total: (result.usage?.input ?? 0) + (result.usage?.output ?? 0)
|
|
2276
|
+
},
|
|
2277
|
+
durationMs: elapsed,
|
|
2278
|
+
fromFallback,
|
|
2279
|
+
stopReason: result.stopReason
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
return {
|
|
2283
|
+
text: "",
|
|
2284
|
+
model: servingModel,
|
|
2285
|
+
provider: servingProviderId,
|
|
2286
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
2287
|
+
durationMs: elapsed,
|
|
2288
|
+
fromFallback,
|
|
2289
|
+
error: lastError instanceof Error ? lastError.message : String(lastError ?? "Unknown error")
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
// ── Private helpers ─────────────────────────────────────────────
|
|
2293
|
+
/**
|
|
2294
|
+
* Resolve the target provider + model from input + config.
|
|
2295
|
+
* Priority: role-based routing > explicit providerId+model > defaults.
|
|
2296
|
+
*/
|
|
2297
|
+
resolveTarget(input, config) {
|
|
2298
|
+
if (input.role && this.opts.modelRouter) {
|
|
2299
|
+
const pick = this.opts.modelRouter.pickForTask(input.role, "");
|
|
2300
|
+
if (pick) {
|
|
2301
|
+
return { providerId: pick.provider, model: pick.model };
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
if (input.providerId && input.model) {
|
|
2305
|
+
return { providerId: input.providerId, model: input.model };
|
|
2306
|
+
}
|
|
2307
|
+
if (input.model) {
|
|
2308
|
+
return { providerId: config.provider, model: input.model };
|
|
2309
|
+
}
|
|
2310
|
+
if (input.providerId) {
|
|
2311
|
+
return { providerId: input.providerId, model: config.model };
|
|
2312
|
+
}
|
|
2313
|
+
if (config.provider && config.model) {
|
|
2314
|
+
return { providerId: config.provider, model: config.model };
|
|
2315
|
+
}
|
|
2316
|
+
return void 0;
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Build a Provider Request from the input.
|
|
2320
|
+
*/
|
|
2321
|
+
buildRequest(input, model) {
|
|
2322
|
+
const messages = [...input.messages ?? []];
|
|
2323
|
+
if (input.userPrompt) {
|
|
2324
|
+
messages.push({ role: "user", content: input.userPrompt });
|
|
2325
|
+
}
|
|
2326
|
+
const system = asTextBlocks(input.system);
|
|
2327
|
+
return {
|
|
2328
|
+
model,
|
|
2329
|
+
...system.length > 0 ? { system } : {},
|
|
2330
|
+
messages,
|
|
2331
|
+
maxTokens: input.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
2332
|
+
...input.temperature !== void 0 ? { temperature: input.temperature } : {},
|
|
2333
|
+
...input.responseFormat ? { responseFormat: input.responseFormat } : {}
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Resolve the abort signal. Provider calls always receive the per-call
|
|
2338
|
+
* timeout, composed with external cancellation when the caller supplies it.
|
|
2339
|
+
*/
|
|
2340
|
+
resolveSignal(input) {
|
|
2341
|
+
const timeoutSignal = AbortSignal.timeout(input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2342
|
+
return input.signal ? AbortSignal.any([input.signal, timeoutSignal]) : timeoutSignal;
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Build the fallback model chain from input + config + current target.
|
|
2346
|
+
*/
|
|
2347
|
+
resolveFallbackChain(input, config, target) {
|
|
2348
|
+
if (input.fallbackModels && input.fallbackModels.length > 0) {
|
|
2349
|
+
return input.fallbackModels;
|
|
2350
|
+
}
|
|
2351
|
+
if (input.fallbackProfile) {
|
|
2352
|
+
const profile = fallbackProfileChain(config, input.fallbackProfile);
|
|
2353
|
+
if (profile.length > 0) return profile;
|
|
2354
|
+
}
|
|
2355
|
+
if (config.fallbackAuto !== false) {
|
|
2356
|
+
return effectiveFallbackChain(config).filter((ref) => {
|
|
2357
|
+
const parsed = parseModelRef(ref);
|
|
2358
|
+
const fbProvider = parsed.provider ?? config.provider;
|
|
2359
|
+
return !(fbProvider === target.providerId && parsed.model === target.model);
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
return [];
|
|
2363
|
+
}
|
|
2364
|
+
/** Attempt a provider call while preserving the actual failure for callers. */
|
|
2365
|
+
async tryCall(provider, request, signal) {
|
|
2366
|
+
try {
|
|
2367
|
+
return {
|
|
2368
|
+
response: await provider.complete(request, { signal }),
|
|
2369
|
+
fallbackEligible: false
|
|
2370
|
+
};
|
|
2371
|
+
} catch (err) {
|
|
2372
|
+
return {
|
|
2373
|
+
error: err,
|
|
2374
|
+
fallbackEligible: !signal.aborted && (!(err instanceof ProviderError) || isFallbackWorthy(err.kind))
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
function asTextBlocks(system) {
|
|
2380
|
+
if (!system) return [];
|
|
2381
|
+
if (Array.isArray(system)) return system;
|
|
2382
|
+
return [{ type: "text", text: system }];
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/tools/one-shot-llm-tool.ts
|
|
2386
|
+
var ONE_SHOT_LLM_TOOL_NAME = "llm";
|
|
2387
|
+
var INPUT_SCHEMA2 = {
|
|
2388
|
+
type: "object",
|
|
2389
|
+
properties: {
|
|
2390
|
+
system: {
|
|
2391
|
+
type: "string",
|
|
2392
|
+
description: "System prompt guiding the LLM behaviour."
|
|
2393
|
+
},
|
|
2394
|
+
userPrompt: {
|
|
2395
|
+
type: "string",
|
|
2396
|
+
description: "Single user turn \u2014 appended as a user-role message."
|
|
2397
|
+
},
|
|
2398
|
+
messages: {
|
|
2399
|
+
type: "array",
|
|
2400
|
+
items: {
|
|
2401
|
+
type: "object",
|
|
2402
|
+
properties: {
|
|
2403
|
+
role: {
|
|
2404
|
+
type: "string",
|
|
2405
|
+
enum: ["system", "user", "assistant", "tool"],
|
|
2406
|
+
description: "Message author role."
|
|
2407
|
+
},
|
|
2408
|
+
content: {
|
|
2409
|
+
type: "string",
|
|
2410
|
+
description: "Message text content."
|
|
2411
|
+
}
|
|
2412
|
+
},
|
|
2413
|
+
required: ["role", "content"],
|
|
2414
|
+
additionalProperties: true
|
|
2415
|
+
},
|
|
2416
|
+
description: "Conversation messages for few-shot examples or multi-turn context. Appended before userPrompt when both are set."
|
|
2417
|
+
},
|
|
2418
|
+
model: {
|
|
2419
|
+
type: "string",
|
|
2420
|
+
description: 'Model id (e.g. "deepseek-chat", "gpt-4o-mini"). Defaults to session model.'
|
|
2421
|
+
},
|
|
2422
|
+
providerId: {
|
|
2423
|
+
type: "string",
|
|
2424
|
+
description: 'Provider id (e.g. "anthropic", "openai"). Defaults to session provider.'
|
|
2425
|
+
},
|
|
2426
|
+
role: {
|
|
2427
|
+
type: "string",
|
|
2428
|
+
description: "Roster role for model-matrix routing. Overrides model/providerId."
|
|
2429
|
+
},
|
|
2430
|
+
fallbackProfile: {
|
|
2431
|
+
type: "string",
|
|
2432
|
+
description: "Named fallback profile from config.fallbackProfiles."
|
|
2433
|
+
},
|
|
2434
|
+
fallbackModels: {
|
|
2435
|
+
type: "array",
|
|
2436
|
+
items: { type: "string" },
|
|
2437
|
+
description: 'Explicit fallback model chain (e.g. ["anthropic/claude-haiku", "openai/gpt-4o-mini"]).'
|
|
2438
|
+
},
|
|
2439
|
+
maxTokens: {
|
|
2440
|
+
type: "number",
|
|
2441
|
+
description: "Maximum output tokens (default 1024)."
|
|
2442
|
+
},
|
|
2443
|
+
responseFormat: {
|
|
2444
|
+
oneOf: [
|
|
2445
|
+
{ type: "string", enum: ["text", "json_object"], description: "Simple response format." },
|
|
2446
|
+
{
|
|
2447
|
+
type: "object",
|
|
2448
|
+
properties: {
|
|
2449
|
+
type: { type: "string", enum: ["json_schema"], description: "Structured JSON output." },
|
|
2450
|
+
json_schema: {
|
|
2451
|
+
type: "object",
|
|
2452
|
+
description: "JSON Schema definition for the structured output."
|
|
2453
|
+
}
|
|
2454
|
+
},
|
|
2455
|
+
required: ["type"],
|
|
2456
|
+
additionalProperties: false
|
|
2457
|
+
}
|
|
2458
|
+
],
|
|
2459
|
+
description: 'Response format: "text" (default), "json_object", or { type: "json_schema", json_schema: {...} }.'
|
|
2460
|
+
},
|
|
2461
|
+
temperature: {
|
|
2462
|
+
type: "number",
|
|
2463
|
+
description: "Sampling temperature."
|
|
2464
|
+
},
|
|
2465
|
+
timeoutMs: {
|
|
2466
|
+
type: "number",
|
|
2467
|
+
description: "Hard timeout in ms (default 30s)."
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
function createOneShotLLMTool(opts) {
|
|
2472
|
+
const orchestrator = new OneShotOrchestrator({
|
|
2473
|
+
buildProvider: opts.buildProvider,
|
|
2474
|
+
getConfig: opts.getConfig,
|
|
2475
|
+
modelRouter: opts.modelRouter,
|
|
2476
|
+
logger: opts.logger
|
|
2477
|
+
});
|
|
2478
|
+
return {
|
|
2479
|
+
name: ONE_SHOT_LLM_TOOL_NAME,
|
|
2480
|
+
description: "Make a one-shot LLM call with a system prompt and user input. Supports provider selection, model routing by role, fallback chains, and timeout. Returns the response text, model info, token usage, and whether a fallback was used. Use this for summarization, classification, extraction, and any single-turn LLM task.",
|
|
2481
|
+
usageHint: "Provide `system` for the instruction and `userPrompt` for the input. Either set `model`+`providerId`, or have defaults configured on the tool. Set `fallbackProfile` or `fallbackModels` for resilience. Check `error` on the result for failure details.",
|
|
2482
|
+
inputSchema: INPUT_SCHEMA2,
|
|
2483
|
+
permission: "auto",
|
|
2484
|
+
mutating: false,
|
|
2485
|
+
async execute(input, _ctx, { signal }) {
|
|
2486
|
+
if (!input.model && !input.providerId && !opts.defaultModel && !opts.defaultProvider) {
|
|
2487
|
+
return {
|
|
2488
|
+
text: "",
|
|
2489
|
+
model: "",
|
|
2490
|
+
provider: "",
|
|
2491
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
2492
|
+
durationMs: 0,
|
|
2493
|
+
fromFallback: false,
|
|
2494
|
+
error: "Either provide `model` and `providerId` in the call, or configure defaultProvider/defaultModel when creating the tool. The `llm` tool does not infer provider/model from the session by default."
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
const effectiveInput = {
|
|
2498
|
+
...input,
|
|
2499
|
+
signal: input.signal ? AbortSignal.any([input.signal, signal]) : signal,
|
|
2500
|
+
model: input.model ?? opts.defaultModel,
|
|
2501
|
+
providerId: input.providerId ?? opts.defaultProvider
|
|
2502
|
+
};
|
|
2503
|
+
return orchestrator.call(effectiveInput);
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
642
2507
|
export {
|
|
643
|
-
|
|
2508
|
+
COUNCIL_TOOL_NAME,
|
|
2509
|
+
MAX_COUNCIL_CONTEXT_CHARS,
|
|
2510
|
+
MAX_COUNCIL_QUESTION_CHARS,
|
|
2511
|
+
MAX_COUNCIL_TOOL_OPTIONS,
|
|
2512
|
+
ONE_SHOT_LLM_TOOL_NAME,
|
|
2513
|
+
createCouncilTool,
|
|
2514
|
+
createMcpControlTool,
|
|
2515
|
+
createOneShotLLMTool
|
|
644
2516
|
};
|
|
645
2517
|
//# sourceMappingURL=index.js.map
|