@mcoda/codali 0.1.108 → 0.1.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/agents/AgentInventory.d.ts +56 -0
  2. package/dist/agents/AgentInventory.d.ts.map +1 -0
  3. package/dist/agents/AgentInventory.js +131 -0
  4. package/dist/agents/RoleResolution.d.ts +49 -0
  5. package/dist/agents/RoleResolution.d.ts.map +1 -0
  6. package/dist/agents/RoleResolution.js +166 -0
  7. package/dist/api/ChatCompletionsAdapter.d.ts +76 -0
  8. package/dist/api/ChatCompletionsAdapter.d.ts.map +1 -0
  9. package/dist/api/ChatCompletionsAdapter.js +54 -0
  10. package/dist/api/CodaliApi.d.ts +78 -0
  11. package/dist/api/CodaliApi.d.ts.map +1 -0
  12. package/dist/api/CodaliApi.js +273 -0
  13. package/dist/api/CodaliServer.d.ts +42 -0
  14. package/dist/api/CodaliServer.d.ts.map +1 -0
  15. package/dist/api/CodaliServer.js +118 -0
  16. package/dist/cli/AskCommand.d.ts +66 -0
  17. package/dist/cli/AskCommand.d.ts.map +1 -0
  18. package/dist/cli/AskCommand.js +536 -0
  19. package/dist/cli/AuthCommand.d.ts +19 -0
  20. package/dist/cli/AuthCommand.d.ts.map +1 -0
  21. package/dist/cli/AuthCommand.js +161 -0
  22. package/dist/cli/ServeCommand.d.ts +5 -0
  23. package/dist/cli/ServeCommand.d.ts.map +1 -0
  24. package/dist/cli/ServeCommand.js +87 -0
  25. package/dist/cli/ToolsCommand.d.ts +22 -0
  26. package/dist/cli/ToolsCommand.d.ts.map +1 -0
  27. package/dist/cli/ToolsCommand.js +208 -0
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +29 -1
  30. package/dist/connectors/http/HttpToolDefinition.d.ts +74 -0
  31. package/dist/connectors/http/HttpToolDefinition.d.ts.map +1 -0
  32. package/dist/connectors/http/HttpToolDefinition.js +190 -0
  33. package/dist/connectors/http/HttpToolSource.d.ts +18 -0
  34. package/dist/connectors/http/HttpToolSource.d.ts.map +1 -0
  35. package/dist/connectors/http/HttpToolSource.js +29 -0
  36. package/dist/connectors/mcp/McpClient.d.ts +132 -0
  37. package/dist/connectors/mcp/McpClient.d.ts.map +1 -0
  38. package/dist/connectors/mcp/McpClient.js +215 -0
  39. package/dist/connectors/mcp/McpServerRegistry.d.ts +75 -0
  40. package/dist/connectors/mcp/McpServerRegistry.d.ts.map +1 -0
  41. package/dist/connectors/mcp/McpServerRegistry.js +180 -0
  42. package/dist/connectors/mcp/McpToolAdapter.d.ts +48 -0
  43. package/dist/connectors/mcp/McpToolAdapter.d.ts.map +1 -0
  44. package/dist/connectors/mcp/McpToolAdapter.js +101 -0
  45. package/dist/connectors/mcp/McpToolSource.d.ts +27 -0
  46. package/dist/connectors/mcp/McpToolSource.d.ts.map +1 -0
  47. package/dist/connectors/mcp/McpToolSource.js +33 -0
  48. package/dist/connectors/media/ImageGenerationTool.d.ts +43 -0
  49. package/dist/connectors/media/ImageGenerationTool.d.ts.map +1 -0
  50. package/dist/connectors/media/ImageGenerationTool.js +122 -0
  51. package/dist/connectors/oauth/DeviceCodeAuth.d.ts +74 -0
  52. package/dist/connectors/oauth/DeviceCodeAuth.d.ts.map +1 -0
  53. package/dist/connectors/oauth/DeviceCodeAuth.js +153 -0
  54. package/dist/docdex/DocdexClient.d.ts +2 -0
  55. package/dist/docdex/DocdexClient.d.ts.map +1 -1
  56. package/dist/docdex/DocdexClient.js +4 -0
  57. package/dist/gateway/CodaliGateway.d.ts +51 -3
  58. package/dist/gateway/CodaliGateway.d.ts.map +1 -1
  59. package/dist/gateway/CodaliGateway.js +226 -2
  60. package/dist/gateway/CodaliGatewayTypes.d.ts +34 -0
  61. package/dist/gateway/CodaliGatewayTypes.d.ts.map +1 -1
  62. package/dist/gateway/EvidenceNormalizer.d.ts.map +1 -1
  63. package/dist/gateway/EvidenceNormalizer.js +90 -1
  64. package/dist/gateway/Finalizer.d.ts +50 -0
  65. package/dist/gateway/Finalizer.d.ts.map +1 -0
  66. package/dist/gateway/Finalizer.js +79 -0
  67. package/dist/gateway/GatewayPlanner.d.ts +13 -0
  68. package/dist/gateway/GatewayPlanner.d.ts.map +1 -1
  69. package/dist/gateway/GatewayPlanner.js +81 -16
  70. package/dist/gateway/GatewayStateMachine.d.ts.map +1 -1
  71. package/dist/gateway/GatewayStateMachine.js +6 -0
  72. package/dist/gateway/GatewayTracer.d.ts +93 -0
  73. package/dist/gateway/GatewayTracer.d.ts.map +1 -0
  74. package/dist/gateway/GatewayTracer.js +214 -0
  75. package/dist/gateway/LocalGatewayProvider.d.ts +46 -0
  76. package/dist/gateway/LocalGatewayProvider.d.ts.map +1 -0
  77. package/dist/gateway/LocalGatewayProvider.js +161 -0
  78. package/dist/gateway/LocalGatewayTaskRunner.d.ts +79 -0
  79. package/dist/gateway/LocalGatewayTaskRunner.d.ts.map +1 -0
  80. package/dist/gateway/LocalGatewayTaskRunner.js +354 -0
  81. package/dist/gateway/RelevanceJudge.d.ts +14 -0
  82. package/dist/gateway/RelevanceJudge.d.ts.map +1 -0
  83. package/dist/gateway/RelevanceJudge.js +87 -0
  84. package/dist/gateway/ResponseSchema.d.ts +32 -0
  85. package/dist/gateway/ResponseSchema.d.ts.map +1 -0
  86. package/dist/gateway/ResponseSchema.js +120 -0
  87. package/dist/gateway/TemporalContext.d.ts +36 -0
  88. package/dist/gateway/TemporalContext.d.ts.map +1 -0
  89. package/dist/gateway/TemporalContext.js +115 -0
  90. package/dist/gateway/ToolCapabilityCompiler.d.ts +5 -0
  91. package/dist/gateway/ToolCapabilityCompiler.d.ts.map +1 -1
  92. package/dist/gateway/ToolCapabilityCompiler.js +27 -2
  93. package/dist/gateway/ToolDescriptorSource.d.ts +6 -0
  94. package/dist/gateway/ToolDescriptorSource.d.ts.map +1 -0
  95. package/dist/gateway/ToolDescriptorSource.js +30 -0
  96. package/dist/gateway/ToolExposure.d.ts +36 -0
  97. package/dist/gateway/ToolExposure.d.ts.map +1 -0
  98. package/dist/gateway/ToolExposure.js +154 -0
  99. package/dist/index.d.ts +57 -0
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +45 -0
  102. package/dist/providers/ClaudeCliProvider.d.ts +2 -0
  103. package/dist/providers/ClaudeCliProvider.d.ts.map +1 -1
  104. package/dist/providers/ClaudeCliProvider.js +2 -0
  105. package/dist/providers/CodexCliProvider.d.ts +2 -0
  106. package/dist/providers/CodexCliProvider.d.ts.map +1 -1
  107. package/dist/providers/CodexCliProvider.js +2 -0
  108. package/dist/providers/MswarmWorkerProvider.d.ts +2 -0
  109. package/dist/providers/MswarmWorkerProvider.d.ts.map +1 -1
  110. package/dist/providers/MswarmWorkerProvider.js +2 -0
  111. package/dist/providers/OllamaRemoteProvider.d.ts +2 -0
  112. package/dist/providers/OllamaRemoteProvider.d.ts.map +1 -1
  113. package/dist/providers/OllamaRemoteProvider.js +2 -0
  114. package/dist/providers/OpenAiCompatibleProvider.d.ts +2 -0
  115. package/dist/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
  116. package/dist/providers/OpenAiCompatibleProvider.js +2 -0
  117. package/dist/providers/ProviderTypes.d.ts +13 -0
  118. package/dist/providers/ProviderTypes.d.ts.map +1 -1
  119. package/dist/providers/ProviderTypes.js +1 -1
  120. package/dist/runcontext/CredentialFile.d.ts +12 -0
  121. package/dist/runcontext/CredentialFile.d.ts.map +1 -0
  122. package/dist/runcontext/CredentialFile.js +81 -0
  123. package/dist/runcontext/RunContextResolver.d.ts +126 -0
  124. package/dist/runcontext/RunContextResolver.d.ts.map +1 -0
  125. package/dist/runcontext/RunContextResolver.js +389 -0
  126. package/dist/storage/CodaliDatasetPrivacyEngine.d.ts +1 -1
  127. package/dist/storage/CodaliDatasetPrivacyEngine.d.ts.map +1 -1
  128. package/dist/storage/CodaliDatasetPrivacyEngine.js +12 -0
  129. package/dist/tools/ToolRegistry.d.ts +14 -1
  130. package/dist/tools/ToolRegistry.d.ts.map +1 -1
  131. package/dist/tools/ToolRegistry.js +38 -1
  132. package/dist/tools/ToolTypes.d.ts +35 -0
  133. package/dist/tools/ToolTypes.d.ts.map +1 -1
  134. package/dist/tools/ToolTypes.js +21 -0
  135. package/dist/tools/TruncateResult.d.ts +29 -0
  136. package/dist/tools/TruncateResult.d.ts.map +1 -0
  137. package/dist/tools/TruncateResult.js +91 -0
  138. package/dist/tools/diff/DiffTool.d.ts.map +1 -1
  139. package/dist/tools/diff/DiffTool.js +2 -0
  140. package/dist/tools/docdex/DocdexTools.d.ts +21 -1
  141. package/dist/tools/docdex/DocdexTools.d.ts.map +1 -1
  142. package/dist/tools/docdex/DocdexTools.js +108 -16
  143. package/dist/tools/filesystem/FileTools.d.ts.map +1 -1
  144. package/dist/tools/filesystem/FileTools.js +8 -0
  145. package/dist/tools/search/SearchTool.d.ts.map +1 -1
  146. package/dist/tools/search/SearchTool.js +3 -1
  147. package/dist/tools/shell/ShellTool.d.ts.map +1 -1
  148. package/dist/tools/shell/ShellTool.js +2 -0
  149. package/package.json +4 -3
@@ -0,0 +1,46 @@
1
+ import type { Provider, ProviderConfig } from "../providers/ProviderTypes.js";
2
+ import type { CodaliGatewayAgentAssignment } from "./AgentTierResolver.js";
3
+ /**
4
+ * Builds a `Provider` from a resolved mcoda agent so the gateway can run
5
+ * in-process from the CLI, without a round trip through mswarm.
6
+ *
7
+ * mswarm keeps its own cloud path; both should end up sharing this factory so
8
+ * local and cloud behaviour cannot drift.
9
+ */
10
+ export interface LocalProviderOptions {
11
+ timeoutMs?: number;
12
+ apiKey?: string;
13
+ }
14
+ /**
15
+ * mcoda adapter -> Codali provider. Adapters not listed fall through to the
16
+ * OpenAI-compatible provider, which is the correct default for local runners
17
+ * (llama.cpp, vLLM, LocalAI) and most hosted endpoints.
18
+ */
19
+ export declare const providerNameForAdapter: (adapter: string | undefined) => string;
20
+ export declare const createProviderForName: (name: string, config: ProviderConfig) => Provider;
21
+ export declare const resolveMswarmApiKey: () => string | undefined;
22
+ /**
23
+ * Creates a provider for an assigned agent. The raw inventory entry carries the
24
+ * transport details (base URL, runner kind, auth mode) that the tier resolver
25
+ * does not model, so it is read here.
26
+ */
27
+ export declare const createProviderForAssignment: (assignment: CodaliGatewayAgentAssignment, options?: LocalProviderOptions) => Provider;
28
+ /**
29
+ * Routes each gateway role to its own provider.
30
+ *
31
+ * Codali's whole premise is that a small model does the errands and a large one
32
+ * writes the answer. A single shared provider would silently collapse that: the
33
+ * planner and the synthesizer would run on the same model and the cost/quality
34
+ * split would be lost. So role dispatch is explicit, with a declared fallback
35
+ * rather than an accidental one.
36
+ */
37
+ export declare class RoleRoutingProvider implements Provider {
38
+ private readonly providers;
39
+ private readonly fallback;
40
+ private readonly resolveRole;
41
+ readonly name = "codali-role-router";
42
+ constructor(providers: Record<string, Provider>, fallback: Provider, resolveRole?: () => string | undefined);
43
+ providerForRole(role: string | undefined): Provider;
44
+ generate(request: Parameters<Provider["generate"]>[0]): ReturnType<Provider["generate"]>;
45
+ }
46
+ //# sourceMappingURL=LocalGatewayProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalGatewayProvider.d.ts","sourceRoot":"","sources":["../../src/gateway/LocalGatewayProvider.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAE3E;;;;;;GAMG;AAEH,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkCD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,MAAM,GAAG,SAAS,KAAG,MAapE,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,QAAQ,cAAc,KACrB,QAWF,CAAC;AAYF,eAAO,MAAM,mBAAmB,QAAO,MAAM,GAAG,SA4B/C,CAAC;AAKF;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,GACtC,YAAY,4BAA4B,EACxC,UAAS,oBAAyB,KACjC,QAoCF,CAAC;AAEF;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,YAAW,QAAQ;IAIhD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAL9B,QAAQ,CAAC,IAAI,wBAAwB;gBAGlB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EACnC,QAAQ,EAAE,QAAQ,EAClB,WAAW,GAAE,MAAM,MAAM,GAAG,SAA2B;IAG1E,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ;IAK7C,QAAQ,CACZ,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,GAC3C,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;CAGpC"}
@@ -0,0 +1,161 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { ClaudeCliProvider } from "../providers/ClaudeCliProvider.js";
5
+ import { CodexCliProvider } from "../providers/CodexCliProvider.js";
6
+ import { OllamaRemoteProvider } from "../providers/OllamaRemoteProvider.js";
7
+ import { OpenAiCompatibleProvider } from "../providers/OpenAiCompatibleProvider.js";
8
+ /**
9
+ * Request timeout for a locally-hosted model.
10
+ *
11
+ * The providers default to 60s, which suits a hosted API and not a local one:
12
+ * qwen3.6 on the self-hosted node takes ~47s for a one-line prompt, so a full
13
+ * context pack aborted every time and surfaced as "the final model was
14
+ * unavailable" — a timeout wearing the costume of an outage.
15
+ */
16
+ const LOCAL_MODEL_TIMEOUT_MS = 600000;
17
+ const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
18
+ const readString = (record, keys) => {
19
+ for (const key of keys) {
20
+ const value = record[key];
21
+ if (typeof value === "string" && value.trim())
22
+ return value.trim();
23
+ }
24
+ return undefined;
25
+ };
26
+ const readBoolean = (record, keys) => {
27
+ for (const key of keys) {
28
+ const value = record[key];
29
+ if (typeof value === "boolean")
30
+ return value;
31
+ }
32
+ return undefined;
33
+ };
34
+ /**
35
+ * mcoda adapter -> Codali provider. Adapters not listed fall through to the
36
+ * OpenAI-compatible provider, which is the correct default for local runners
37
+ * (llama.cpp, vLLM, LocalAI) and most hosted endpoints.
38
+ */
39
+ export const providerNameForAdapter = (adapter) => {
40
+ switch ((adapter ?? "").toLowerCase()) {
41
+ case "claude-cli":
42
+ return "claude-cli";
43
+ case "codex-cli":
44
+ return "codex-cli";
45
+ case "ollama-cli":
46
+ case "ollama-remote":
47
+ case "ollama":
48
+ return "ollama";
49
+ default:
50
+ return "openai-compatible";
51
+ }
52
+ };
53
+ export const createProviderForName = (name, config) => {
54
+ switch (name) {
55
+ case "claude-cli":
56
+ return new ClaudeCliProvider(config);
57
+ case "codex-cli":
58
+ return new CodexCliProvider(config);
59
+ case "ollama":
60
+ return new OllamaRemoteProvider(config);
61
+ default:
62
+ return new OpenAiCompatibleProvider(config);
63
+ }
64
+ };
65
+ /**
66
+ * The mswarm API key, used to reach self-hosted agents served through the
67
+ * mswarm gateway. `mcoda agent list --json` reports `auth.configured: true` but
68
+ * never discloses the key itself, so it is read from local trusted config.
69
+ *
70
+ * Local CLI only. A multi-tenant deployment must receive credentials from its
71
+ * host rather than reading the operator's files.
72
+ */
73
+ let cachedMswarmKey;
74
+ export const resolveMswarmApiKey = () => {
75
+ if (cachedMswarmKey !== undefined)
76
+ return cachedMswarmKey ?? undefined;
77
+ const fromEnv = process.env.MSWARM_API_KEY?.trim();
78
+ if (fromEnv) {
79
+ cachedMswarmKey = fromEnv;
80
+ return fromEnv;
81
+ }
82
+ const configPath = path.join(homedir(), ".docdex", "config.toml");
83
+ if (existsSync(configPath)) {
84
+ try {
85
+ const raw = readFileSync(configPath, "utf8");
86
+ // Minimal TOML read: find api_key inside [integrations.mswarm].
87
+ const section = raw.split(/^\[integrations\.mswarm\]\s*$/m)[1];
88
+ const match = section?.match(/^\s*api_key\s*=\s*"([^"]+)"/m);
89
+ if (match?.[1]) {
90
+ cachedMswarmKey = match[1];
91
+ return cachedMswarmKey;
92
+ }
93
+ }
94
+ catch {
95
+ // Unreadable config is not fatal; the provider will fail with a clearer
96
+ // authentication error than anything we could raise here.
97
+ }
98
+ }
99
+ cachedMswarmKey = null;
100
+ return undefined;
101
+ };
102
+ const isMswarmEndpoint = (baseUrl) => Boolean(baseUrl && /(^|\/\/)([^/]*\.)?mswarm\.org/i.test(baseUrl));
103
+ /**
104
+ * Creates a provider for an assigned agent. The raw inventory entry carries the
105
+ * transport details (base URL, runner kind, auth mode) that the tier resolver
106
+ * does not model, so it is read here.
107
+ */
108
+ export const createProviderForAssignment = (assignment, options = {}) => {
109
+ const raw = isRecord(assignment.candidate.raw) ? assignment.candidate.raw : {};
110
+ // mcoda nests transport details under `config`; older entries put them at the
111
+ // top level. Read both, preferring the nested form.
112
+ const nested = isRecord(raw.config) ? raw.config : {};
113
+ const adapter = assignment.candidate.adapter ?? readString(raw, ["adapter"]);
114
+ const providerName = providerNameForAdapter(adapter);
115
+ const baseUrl = readString(nested, ["baseUrl", "base_url", "apiBaseUrl", "api_base_url"]) ??
116
+ readString(raw, ["baseUrl", "base_url"]);
117
+ const apiKey = options.apiKey ??
118
+ readString(nested, ["apiKey", "api_key"]) ??
119
+ readString(raw, ["apiKey", "api_key"]) ??
120
+ (isMswarmEndpoint(baseUrl) ? resolveMswarmApiKey() : undefined);
121
+ const config = {
122
+ model: assignment.candidate.model ?? readString(raw, ["defaultModel", "model"]) ?? "",
123
+ baseUrl,
124
+ apiKey,
125
+ timeoutMs: options.timeoutMs ?? LOCAL_MODEL_TIMEOUT_MS,
126
+ runnerKind: readString(nested, ["runnerKind", "runner_kind"]),
127
+ authMode: readString(nested, ["authMode", "auth_mode"]),
128
+ localRunner: isRecord(nested.localRunner)
129
+ ? nested.localRunner
130
+ : undefined,
131
+ supportsTools: assignment.candidate.supportsTools ?? readBoolean(raw, ["supportsTools"]),
132
+ supportsJsonSchema: assignment.candidate.supportsJsonSchema ?? readBoolean(raw, ["supportsJsonSchema"]),
133
+ supportsStreaming: assignment.candidate.supportsStreaming ?? readBoolean(raw, ["supportsStreaming"]),
134
+ };
135
+ return createProviderForName(providerName, config);
136
+ };
137
+ /**
138
+ * Routes each gateway role to its own provider.
139
+ *
140
+ * Codali's whole premise is that a small model does the errands and a large one
141
+ * writes the answer. A single shared provider would silently collapse that: the
142
+ * planner and the synthesizer would run on the same model and the cost/quality
143
+ * split would be lost. So role dispatch is explicit, with a declared fallback
144
+ * rather than an accidental one.
145
+ */
146
+ export class RoleRoutingProvider {
147
+ constructor(providers, fallback, resolveRole = () => undefined) {
148
+ this.providers = providers;
149
+ this.fallback = fallback;
150
+ this.resolveRole = resolveRole;
151
+ this.name = "codali-role-router";
152
+ }
153
+ providerForRole(role) {
154
+ if (!role)
155
+ return this.fallback;
156
+ return this.providers[role] ?? this.fallback;
157
+ }
158
+ async generate(request) {
159
+ return this.providerForRole(this.resolveRole()).generate(request);
160
+ }
161
+ }
@@ -0,0 +1,79 @@
1
+ import type { Provider } from "../providers/ProviderTypes.js";
2
+ import type { ToolRegistry } from "../tools/ToolRegistry.js";
3
+ import type { ToolContext } from "../tools/ToolTypes.js";
4
+ import type { CodaliGatewayWorkerTaskRunInput, CodaliGatewayWorkerTaskRunResult, CodaliGatewayWorkerTaskRunner } from "./GatewayStateMachine.js";
5
+ /**
6
+ * Bounded worker-task runner.
7
+ *
8
+ * ## Why this exists
9
+ *
10
+ * Codali previously executed every gateway worker task by handing it to the
11
+ * full `CodaliRuntime` agent loop (see `createGatewayTaskRunner` in
12
+ * `packages/mswarm/src/codali-executor.ts`). That nested one open-ended loop
13
+ * inside another: the gateway decided how many *rounds* to run, while the
14
+ * runtime independently decided how many *steps* to take inside each task.
15
+ * Neither could bound the other, so a single request could fan out
16
+ * unpredictably and a runaway task was near-impossible to attribute.
17
+ *
18
+ * This runner removes the inner loop. Exactly one component - the gateway state
19
+ * machine - owns iteration. A task here is a single bounded pass:
20
+ *
21
+ * 1. ask the model what it needs (one model call)
22
+ * 2. execute the tool calls it asked for, in parallel where independent
23
+ * 3. ask the model to turn the results into the task output (one model call)
24
+ *
25
+ * There is no step 4 and no "decide whether to keep going". If the evidence is
26
+ * insufficient, that is the verifier's judgement to make, and the gateway will
27
+ * schedule another round against `maxIterations`. A worker never extends its
28
+ * own budget.
29
+ *
30
+ * The upper bound on work per task is therefore fixed and knowable:
31
+ * 2 model calls and `min(remainingToolCalls, MAX_TOOL_CALLS_PER_TASK)` tool
32
+ * calls.
33
+ */
34
+ /** Hard ceiling on tool calls in a single task, independent of run budget. */
35
+ export declare const MAX_TOOL_CALLS_PER_TASK = 8;
36
+ /** Model calls per task: one to select tools, one to summarize results. */
37
+ export declare const MAX_MODEL_CALLS_PER_TASK = 2;
38
+ export interface LocalGatewayTaskRunnerOptions {
39
+ provider: Provider;
40
+ registry: ToolRegistry;
41
+ toolContext: ToolContext;
42
+ maxTokens?: number;
43
+ temperature?: number;
44
+ /** Observability hook; see `GatewayTracer`. */
45
+ onEvent?: (event: LocalGatewayTaskRunnerEvent) => void;
46
+ }
47
+ export type LocalGatewayTaskRunnerEvent = {
48
+ type: "task_start";
49
+ taskId: string;
50
+ workerRole: string;
51
+ allowedTools: string[];
52
+ } | {
53
+ type: "tool_call";
54
+ taskId: string;
55
+ tool: string;
56
+ args: unknown;
57
+ } | {
58
+ type: "tool_result";
59
+ taskId: string;
60
+ tool: string;
61
+ ok: boolean;
62
+ latencyMs: number;
63
+ errorCode?: string;
64
+ } | {
65
+ type: "task_end";
66
+ taskId: string;
67
+ status: "succeeded" | "failed";
68
+ toolCallCount: number;
69
+ modelCallCount: number;
70
+ durationMs: number;
71
+ };
72
+ export declare class LocalGatewayTaskRunner implements CodaliGatewayWorkerTaskRunner {
73
+ private readonly options;
74
+ constructor(options: LocalGatewayTaskRunnerOptions);
75
+ run(input: CodaliGatewayWorkerTaskRunInput): Promise<CodaliGatewayWorkerTaskRunResult>;
76
+ }
77
+ export declare const createLocalGatewayTaskRunner: (options: LocalGatewayTaskRunnerOptions) => LocalGatewayTaskRunner;
78
+ export declare const isRecordValue: (value: unknown) => value is Record<string, unknown>;
79
+ //# sourceMappingURL=LocalGatewayTaskRunner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalGatewayTaskRunner.d.ts","sourceRoot":"","sources":["../../src/gateway/LocalGatewayTaskRunner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAGT,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,KAAK,EAEV,+BAA+B,EAC/B,gCAAgC,EAChC,6BAA6B,EAE9B,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAe1C,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;IACvB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,2BAA2B,KAAK,IAAI,CAAC;CACxD;AAED,MAAM,MAAM,2BAA2B,GACnC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GAClF;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAClE;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AA6DN,qBAAa,sBAAuB,YAAW,6BAA6B;IAC9D,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,6BAA6B;IAE7D,GAAG,CACP,KAAK,EAAE,+BAA+B,GACrC,OAAO,CAAC,gCAAgC,CAAC;CA+S7C;AAED,eAAO,MAAM,4BAA4B,GACvC,SAAS,6BAA6B,KACrC,sBAA6D,CAAC;AAEjE,eAAO,MAAM,aAAa,UArXD,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAqX7B,CAAC"}
@@ -0,0 +1,354 @@
1
+ import { CODALI_GATEWAY_SECURITY_PROMPT_HARDENING } from "./GatewaySecurityPolicy.js";
2
+ /**
3
+ * Bounded worker-task runner.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * Codali previously executed every gateway worker task by handing it to the
8
+ * full `CodaliRuntime` agent loop (see `createGatewayTaskRunner` in
9
+ * `packages/mswarm/src/codali-executor.ts`). That nested one open-ended loop
10
+ * inside another: the gateway decided how many *rounds* to run, while the
11
+ * runtime independently decided how many *steps* to take inside each task.
12
+ * Neither could bound the other, so a single request could fan out
13
+ * unpredictably and a runaway task was near-impossible to attribute.
14
+ *
15
+ * This runner removes the inner loop. Exactly one component - the gateway state
16
+ * machine - owns iteration. A task here is a single bounded pass:
17
+ *
18
+ * 1. ask the model what it needs (one model call)
19
+ * 2. execute the tool calls it asked for, in parallel where independent
20
+ * 3. ask the model to turn the results into the task output (one model call)
21
+ *
22
+ * There is no step 4 and no "decide whether to keep going". If the evidence is
23
+ * insufficient, that is the verifier's judgement to make, and the gateway will
24
+ * schedule another round against `maxIterations`. A worker never extends its
25
+ * own budget.
26
+ *
27
+ * The upper bound on work per task is therefore fixed and knowable:
28
+ * 2 model calls and `min(remainingToolCalls, MAX_TOOL_CALLS_PER_TASK)` tool
29
+ * calls.
30
+ */
31
+ /** Hard ceiling on tool calls in a single task, independent of run budget. */
32
+ export const MAX_TOOL_CALLS_PER_TASK = 8;
33
+ /** Model calls per task: one to select tools, one to summarize results. */
34
+ export const MAX_MODEL_CALLS_PER_TASK = 2;
35
+ const DEFAULT_TASK_TIMEOUT_MS = 60000;
36
+ const DEFAULT_MAX_TOKENS = 1200;
37
+ const DEFAULT_TEMPERATURE = 0;
38
+ /**
39
+ * Tool output beyond this is truncated before it reaches the model.
40
+ *
41
+ * Connector list endpoints are the pressure here: a GitHub `list_commits` for a
42
+ * fortnight of activity runs well past the old 8k cap, and the model then
43
+ * summarized only the commits that survived — accurately, but incompletely.
44
+ */
45
+ const MAX_TOOL_OUTPUT_CHARS = 24000;
46
+ const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
47
+ const truncate = (value, limit = MAX_TOOL_OUTPUT_CHARS) => value.length > limit
48
+ ? `${value.slice(0, limit)}\n\n[TRUNCATED: ${value.length - limit} more characters were cut. This result is INCOMPLETE — say so explicitly in your summary and do not present it as the full set.]`
49
+ : value;
50
+ const positiveInteger = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value > 0
51
+ ? Math.floor(value)
52
+ : fallback;
53
+ const nonNegativeInteger = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value >= 0
54
+ ? Math.floor(value)
55
+ : fallback;
56
+ const errorMessageFor = (error) => error instanceof Error ? error.message : String(error);
57
+ /**
58
+ * Tool definitions offered to the worker model, narrowed to what the planner
59
+ * allowed for this task. A tool absent from `allowedTools` is not merely
60
+ * discouraged - the model never sees it.
61
+ */
62
+ const toolDefinitionsFor = (registry, allowedTools) => registry.catalog(allowedTools).map((descriptor) => ({
63
+ name: descriptor.name,
64
+ description: descriptor.description,
65
+ inputSchema: descriptor.inputSchema,
66
+ }));
67
+ const buildTaskMessages = (input, hasTools) => {
68
+ const system = [
69
+ "You are a Codali gateway worker.",
70
+ "Gather evidence for one bounded sub-task. Do not answer the user's overall question.",
71
+ hasTools
72
+ ? "Call the tools you need in a single batch. You get exactly one opportunity to call tools, so request everything you need at once."
73
+ : "No tools are available. Answer from the task description alone or state what is missing.",
74
+ "Report only what the tool results actually support. Never invent file paths, identifiers, figures, or quotations.",
75
+ "If the results are insufficient, say so plainly instead of guessing; another round may be scheduled.",
76
+ CODALI_GATEWAY_SECURITY_PROMPT_HARDENING.toolOutputBoundary,
77
+ CODALI_GATEWAY_SECURITY_PROMPT_HARDENING.policyImmutability,
78
+ CODALI_GATEWAY_SECURITY_PROMPT_HARDENING.tenantScope,
79
+ ].join("\n");
80
+ return [
81
+ { role: "system", content: system },
82
+ { role: "user", content: input.prompt },
83
+ ];
84
+ };
85
+ export class LocalGatewayTaskRunner {
86
+ constructor(options) {
87
+ this.options = options;
88
+ }
89
+ async run(input) {
90
+ const startedMs = Date.now();
91
+ const taskId = input.task.id;
92
+ const deadlineMs = startedMs + positiveInteger(input.timeoutMs, DEFAULT_TASK_TIMEOUT_MS);
93
+ const allowedTools = input.allowedTools ?? [];
94
+ const toolBudget = Math.min(nonNegativeInteger(input.remainingToolCalls, 0), MAX_TOOL_CALLS_PER_TASK);
95
+ const modelBudget = Math.min(nonNegativeInteger(input.remainingModelCalls, MAX_MODEL_CALLS_PER_TASK), MAX_MODEL_CALLS_PER_TASK);
96
+ const toolCalls = [];
97
+ const modelCalls = [];
98
+ this.options.onEvent?.({
99
+ type: "task_start",
100
+ taskId,
101
+ workerRole: input.task.workerRole,
102
+ allowedTools: [...allowedTools],
103
+ });
104
+ const finish = (result) => {
105
+ this.options.onEvent?.({
106
+ type: "task_end",
107
+ taskId,
108
+ status: result.status,
109
+ toolCallCount: toolCalls.length,
110
+ modelCallCount: modelCalls.length,
111
+ durationMs: Date.now() - startedMs,
112
+ });
113
+ return { ...result, toolCalls, modelCalls };
114
+ };
115
+ if (modelBudget <= 0) {
116
+ return finish({
117
+ status: "failed",
118
+ errorCode: "GATEWAY_MODEL_BUDGET_EXCEEDED",
119
+ errorMessage: "No model calls remain for this worker task.",
120
+ });
121
+ }
122
+ const offerTools = toolBudget > 0 && allowedTools.length > 0;
123
+ const toolDefinitions = offerTools
124
+ ? toolDefinitionsFor(this.options.registry, allowedTools)
125
+ : [];
126
+ const messages = buildTaskMessages(input, toolDefinitions.length > 0);
127
+ // ---- Pass 1: ask the model what it needs -----------------------------
128
+ let selection;
129
+ const selectionStartedMs = Date.now();
130
+ try {
131
+ selection = await this.options.provider.generate({
132
+ messages,
133
+ tools: toolDefinitions.length > 0 ? toolDefinitions : undefined,
134
+ toolChoice: toolDefinitions.length > 0 ? "auto" : "none",
135
+ maxTokens: positiveInteger(this.options.maxTokens, DEFAULT_MAX_TOKENS),
136
+ temperature: this.options.temperature ?? DEFAULT_TEMPERATURE,
137
+ });
138
+ }
139
+ catch (error) {
140
+ modelCalls.push({
141
+ role: "worker",
142
+ status: "failed",
143
+ latencyMs: Date.now() - selectionStartedMs,
144
+ provider: this.options.provider.name,
145
+ errorCode: "GATEWAY_WORKER_MODEL_FAILED",
146
+ errorMessage: errorMessageFor(error),
147
+ });
148
+ return finish({
149
+ status: "failed",
150
+ errorCode: "GATEWAY_WORKER_MODEL_FAILED",
151
+ errorMessage: errorMessageFor(error),
152
+ });
153
+ }
154
+ modelCalls.push({
155
+ role: "worker",
156
+ status: "success",
157
+ latencyMs: Date.now() - selectionStartedMs,
158
+ provider: this.options.provider.name,
159
+ output: selection.message.content,
160
+ metadata: {
161
+ pass: "tool_selection",
162
+ requestedToolCalls: selection.toolCalls?.length ?? 0,
163
+ usage: selection.usage,
164
+ },
165
+ });
166
+ const requested = (selection.toolCalls ?? []).slice(0, toolBudget);
167
+ const droppedForBudget = (selection.toolCalls ?? []).length - requested.length;
168
+ // No tools requested: the first answer is the task output. One model call,
169
+ // and we return rather than looping to look for more work.
170
+ if (requested.length === 0) {
171
+ const output = selection.message.content.trim();
172
+ return finish({
173
+ status: "succeeded",
174
+ output,
175
+ metadata: {
176
+ pass: "direct",
177
+ modelCallCount: modelCalls.length,
178
+ toolCallCount: 0,
179
+ ...(droppedForBudget > 0 ? { droppedToolCalls: droppedForBudget } : {}),
180
+ },
181
+ });
182
+ }
183
+ // ---- Execute the batch ------------------------------------------------
184
+ // Independent read-only calls run in parallel. Ordering between them is not
185
+ // meaningful; each is validated against the registry schema before running.
186
+ const executions = await Promise.all(requested.map(async (call) => {
187
+ if (Date.now() > deadlineMs) {
188
+ return {
189
+ call,
190
+ record: {
191
+ tool: call.name,
192
+ status: "failed",
193
+ latencyMs: 0,
194
+ args: call.args,
195
+ errorCode: "tool_timeout",
196
+ errorMessage: "Task deadline elapsed before the tool was invoked.",
197
+ },
198
+ output: "Tool skipped: task deadline elapsed.",
199
+ };
200
+ }
201
+ this.options.onEvent?.({ type: "tool_call", taskId, tool: call.name, args: call.args });
202
+ const toolStartedMs = Date.now();
203
+ if (!allowedTools.includes(call.name)) {
204
+ // The model named a tool outside the planner's allowance. Refuse it
205
+ // and tell the model, rather than silently widening the policy.
206
+ const latencyMs = Date.now() - toolStartedMs;
207
+ this.options.onEvent?.({
208
+ type: "tool_result",
209
+ taskId,
210
+ tool: call.name,
211
+ ok: false,
212
+ latencyMs,
213
+ errorCode: "tool_permission_denied",
214
+ });
215
+ return {
216
+ call,
217
+ record: {
218
+ tool: call.name,
219
+ status: "failed",
220
+ latencyMs,
221
+ args: call.args,
222
+ errorCode: "tool_permission_denied",
223
+ errorMessage: `Tool ${call.name} is not allowed for this task.`,
224
+ },
225
+ output: `Tool ${call.name} is not allowed for this task.`,
226
+ };
227
+ }
228
+ const result = await this.options.registry.execute(call.name, call.args, this.options.toolContext);
229
+ const latencyMs = Date.now() - toolStartedMs;
230
+ this.options.onEvent?.({
231
+ type: "tool_result",
232
+ taskId,
233
+ tool: call.name,
234
+ ok: result.ok,
235
+ latencyMs,
236
+ errorCode: result.error?.code,
237
+ });
238
+ return {
239
+ call,
240
+ record: {
241
+ tool: call.name,
242
+ status: result.ok ? "success" : "failed",
243
+ latencyMs,
244
+ args: call.args,
245
+ result: result.ok ? (result.data ?? result.output) : undefined,
246
+ errorCode: result.error?.code,
247
+ errorMessage: result.error?.message,
248
+ },
249
+ output: result.ok
250
+ ? truncate(result.output)
251
+ : `Tool failed (${result.error?.code ?? "unknown"}): ${result.error?.message ?? ""}`,
252
+ };
253
+ }));
254
+ for (const execution of executions) {
255
+ toolCalls.push(execution.record);
256
+ }
257
+ const succeededCount = executions.filter((execution) => execution.record.status === "success").length;
258
+ // Every tool failed: report the failure rather than asking the model to
259
+ // narrate an empty result set, which is where fabrication starts.
260
+ if (succeededCount === 0) {
261
+ const first = executions[0]?.record;
262
+ return finish({
263
+ status: "failed",
264
+ errorCode: first?.errorCode ?? "GATEWAY_WORKER_TOOLS_FAILED",
265
+ errorMessage: first?.errorMessage ?? "All tool calls for this worker task failed.",
266
+ metadata: { pass: "tools_failed", toolCallCount: toolCalls.length },
267
+ });
268
+ }
269
+ if (modelCalls.length >= modelBudget) {
270
+ // No budget left to summarize. Return the raw tool outputs as the task
271
+ // output; the finalizer, not the user, is what sees this.
272
+ return finish({
273
+ status: "succeeded",
274
+ output: executions.map((execution) => `${execution.call.name}: ${execution.output}`).join("\n\n"),
275
+ metadata: {
276
+ pass: "unsummarized",
277
+ reason: "model_budget_exhausted",
278
+ toolCallCount: toolCalls.length,
279
+ },
280
+ });
281
+ }
282
+ // ---- Pass 2: turn the results into the task output --------------------
283
+ const resultMessages = [
284
+ ...messages,
285
+ {
286
+ role: "assistant",
287
+ content: selection.message.content ||
288
+ `Requested tools: ${requested.map((call) => call.name).join(", ")}`,
289
+ },
290
+ {
291
+ role: "user",
292
+ content: [
293
+ "Tool results:",
294
+ ...executions.map((execution) => `--- ${execution.call.name} ---\n${execution.output}`),
295
+ "",
296
+ "Summarize what these results establish for this sub-task.",
297
+ "If any result is marked TRUNCATED, state that the data is incomplete and roughly what is missing. Never present a truncated list as complete.",
298
+ "Attribute each statement to the tool that produced it.",
299
+ "State explicitly what remains unknown. Do not speculate beyond the results.",
300
+ ].join("\n"),
301
+ },
302
+ ];
303
+ const summaryStartedMs = Date.now();
304
+ try {
305
+ const summary = await this.options.provider.generate({
306
+ messages: resultMessages,
307
+ toolChoice: "none",
308
+ maxTokens: positiveInteger(this.options.maxTokens, DEFAULT_MAX_TOKENS),
309
+ temperature: this.options.temperature ?? DEFAULT_TEMPERATURE,
310
+ });
311
+ modelCalls.push({
312
+ role: "worker",
313
+ status: "success",
314
+ latencyMs: Date.now() - summaryStartedMs,
315
+ provider: this.options.provider.name,
316
+ output: summary.message.content,
317
+ metadata: { pass: "summary", usage: summary.usage },
318
+ });
319
+ return finish({
320
+ status: "succeeded",
321
+ output: summary.message.content.trim(),
322
+ metadata: {
323
+ pass: "tools_summarized",
324
+ toolCallCount: toolCalls.length,
325
+ succeededToolCalls: succeededCount,
326
+ ...(droppedForBudget > 0 ? { droppedToolCalls: droppedForBudget } : {}),
327
+ },
328
+ });
329
+ }
330
+ catch (error) {
331
+ modelCalls.push({
332
+ role: "worker",
333
+ status: "failed",
334
+ latencyMs: Date.now() - summaryStartedMs,
335
+ provider: this.options.provider.name,
336
+ errorCode: "GATEWAY_WORKER_SUMMARY_FAILED",
337
+ errorMessage: errorMessageFor(error),
338
+ });
339
+ // Summarization failed but the evidence is real. Return it unsummarized
340
+ // rather than discarding a successful tool batch.
341
+ return finish({
342
+ status: "succeeded",
343
+ output: executions.map((execution) => `${execution.call.name}: ${execution.output}`).join("\n\n"),
344
+ metadata: {
345
+ pass: "unsummarized",
346
+ reason: "summary_failed",
347
+ toolCallCount: toolCalls.length,
348
+ },
349
+ });
350
+ }
351
+ }
352
+ }
353
+ export const createLocalGatewayTaskRunner = (options) => new LocalGatewayTaskRunner(options);
354
+ export const isRecordValue = isRecord;
@@ -0,0 +1,14 @@
1
+ import type { Provider } from "../providers/ProviderTypes.js";
2
+ import type { RelevanceJudge } from "../tools/docdex/DocdexTools.js";
3
+ /**
4
+ * Reduces a result payload to a short list of titles. The judge needs to know
5
+ * *what* was found, not the contents — sending snippets would cost more than
6
+ * the web call being avoided.
7
+ */
8
+ export declare const summarizeResultTitles: (results: unknown) => string[];
9
+ export interface RelevanceJudgeOptions {
10
+ provider: Provider;
11
+ maxTokens?: number;
12
+ }
13
+ export declare const createRelevanceJudge: (options: RelevanceJudgeOptions) => RelevanceJudge;
14
+ //# sourceMappingURL=RelevanceJudge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RelevanceJudge.d.ts","sourceRoot":"","sources":["../../src/gateway/RelevanceJudge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAyBrE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,OAAO,KAAG,MAAM,EAqB9D,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,oBAAoB,GAC/B,SAAS,qBAAqB,KAC7B,cAsCF,CAAC"}