@desplega.ai/agent-swarm 1.79.4 → 1.80.1
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/openapi.json +496 -32
- package/package.json +14 -6
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +102 -31
- package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/pricing-normalize.ts +81 -0
- package/src/be/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -0
- package/src/be/seed-pricing.ts +293 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +21 -4
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +663 -246
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/context.ts +6 -2
- package/src/http/index.ts +126 -68
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +381 -0
- package/src/http/session-data.ts +74 -23
- package/src/linear/outbound.ts +9 -2
- package/src/otel-impl.ts +200 -0
- package/src/otel.ts +132 -0
- package/src/providers/claude-adapter.ts +52 -6
- package/src/providers/claude-managed-adapter.ts +43 -17
- package/src/providers/claude-managed-pricing.ts +34 -0
- package/src/providers/codex-adapter.ts +38 -27
- package/src/providers/codex-models.ts +22 -3
- package/src/providers/devin-adapter.ts +11 -0
- package/src/providers/opencode-adapter.ts +31 -7
- package/src/providers/pi-mono-adapter.ts +39 -7
- package/src/providers/pricing-sources.md +52 -0
- package/src/providers/swarm-events-shared.ts +8 -4
- package/src/providers/types.ts +33 -10
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +18 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/claude-managed-adapter.test.ts +17 -3
- package/src/tests/claude-managed-setup.test.ts +10 -1
- package/src/tests/codex-adapter.test.ts +20 -19
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/context-snapshot.test.ts +2 -2
- package/src/tests/context-window.test.ts +65 -1
- package/src/tests/devin-adapter.test.ts +2 -0
- package/src/tests/http/context-routes.test.ts +161 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/migration-063-schema-relax.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +146 -1
- package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
- package/src/tests/pages-view-count.test.ts +30 -5
- package/src/tests/providers/codex-cost.test.ts +18 -0
- package/src/tests/providers/opencode-cost.test.ts +74 -0
- package/src/tests/providers/pi-cost.test.ts +128 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +350 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +289 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +54 -1
- package/src/tests/session-costs-codex-recompute.test.ts +35 -22
- package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
- package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
- package/src/tests/store-progress-cost.test.ts +6 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/store-progress.ts +16 -60
- package/src/tools/tool-config.ts +7 -0
- package/src/tools/utils.ts +65 -12
- package/src/types.ts +122 -10
- package/src/utils/api-key.ts +28 -0
- package/src/utils/context-window.ts +104 -4
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +29 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getAgentById } from "../be/db";
|
|
4
|
+
import { createEvent } from "../be/events";
|
|
5
|
+
import { deleteScript, getScript, upsertScriptByName } from "../be/scripts/db";
|
|
6
|
+
import { searchScripts } from "../be/scripts/embeddings";
|
|
7
|
+
import { SCRIPT_SDK_TYPES, SCRIPT_STDLIB_TYPES, typecheckScript } from "../be/scripts/typecheck";
|
|
8
|
+
import { extractScriptSignature } from "../scripts-runtime/extract-signature";
|
|
9
|
+
import { runScript } from "../scripts-runtime/loader";
|
|
10
|
+
import {
|
|
11
|
+
ScriptFsModeSchema,
|
|
12
|
+
type ScriptRecord,
|
|
13
|
+
type ScriptScope,
|
|
14
|
+
ScriptScopeSchema,
|
|
15
|
+
} from "../types";
|
|
16
|
+
import { scrubObject } from "../utils/secret-scrubber";
|
|
17
|
+
import { route } from "./route-def";
|
|
18
|
+
import { json, jsonError } from "./utils";
|
|
19
|
+
|
|
20
|
+
const scriptNameSchema = z.string().min(1).max(200);
|
|
21
|
+
|
|
22
|
+
const upsertBodySchema = z.object({
|
|
23
|
+
name: scriptNameSchema,
|
|
24
|
+
source: z.string().min(1),
|
|
25
|
+
description: z.string().default(""),
|
|
26
|
+
intent: z.string().default(""),
|
|
27
|
+
scope: ScriptScopeSchema.default("agent"),
|
|
28
|
+
fsMode: ScriptFsModeSchema.default("none"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const runBodySchema = z
|
|
32
|
+
.object({
|
|
33
|
+
name: scriptNameSchema.optional(),
|
|
34
|
+
source: z.string().min(1).optional(),
|
|
35
|
+
args: z.unknown().optional(),
|
|
36
|
+
intent: z.string().default(""),
|
|
37
|
+
scope: ScriptScopeSchema.optional(),
|
|
38
|
+
fsMode: ScriptFsModeSchema.default("none"),
|
|
39
|
+
})
|
|
40
|
+
.refine((body) => Boolean(body.name) !== Boolean(body.source), {
|
|
41
|
+
message: "Provide exactly one of name or source",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const searchBodySchema = z.object({
|
|
45
|
+
query: z.string().default(""),
|
|
46
|
+
scope: ScriptScopeSchema.optional(),
|
|
47
|
+
limit: z.number().int().min(1).max(100).default(10),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const nameParamsSchema = z.object({ name: scriptNameSchema });
|
|
51
|
+
const scopeQuerySchema = z.object({ scope: ScriptScopeSchema.default("agent") });
|
|
52
|
+
const optionalScopeQuerySchema = z.object({ scope: ScriptScopeSchema.optional() });
|
|
53
|
+
|
|
54
|
+
const upsertRoute = route({
|
|
55
|
+
method: "post",
|
|
56
|
+
path: "/api/scripts/upsert",
|
|
57
|
+
pattern: ["api", "scripts", "upsert"],
|
|
58
|
+
operationId: "scripts_upsert",
|
|
59
|
+
summary: "Create or update a reusable script",
|
|
60
|
+
description: "Explicit script upserts run a TypeScript typecheck before writing.",
|
|
61
|
+
tags: ["Scripts"],
|
|
62
|
+
body: upsertBodySchema,
|
|
63
|
+
responses: {
|
|
64
|
+
200: { description: "Script upserted" },
|
|
65
|
+
400: { description: "Validation or typecheck failure" },
|
|
66
|
+
403: { description: "Global write requires lead agent" },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const runRoute = route({
|
|
71
|
+
method: "post",
|
|
72
|
+
path: "/api/scripts/run",
|
|
73
|
+
pattern: ["api", "scripts", "run"],
|
|
74
|
+
operationId: "scripts_run",
|
|
75
|
+
summary: "Run a reusable or inline script",
|
|
76
|
+
description:
|
|
77
|
+
"Inline source skips typecheck and is auto-saved as a scratch script only on success.",
|
|
78
|
+
tags: ["Scripts"],
|
|
79
|
+
body: runBodySchema,
|
|
80
|
+
responses: {
|
|
81
|
+
200: { description: "Script run completed" },
|
|
82
|
+
400: { description: "Validation error" },
|
|
83
|
+
404: { description: "Script not found" },
|
|
84
|
+
501: { description: "workspace-rw scripts are not supported in v1" },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const searchRoute = route({
|
|
89
|
+
method: "post",
|
|
90
|
+
path: "/api/scripts/search",
|
|
91
|
+
pattern: ["api", "scripts", "search"],
|
|
92
|
+
operationId: "scripts_search",
|
|
93
|
+
summary: "Search reusable scripts",
|
|
94
|
+
description: "Phase 3 search is substring-only over script name and metadata.",
|
|
95
|
+
tags: ["Scripts"],
|
|
96
|
+
body: searchBodySchema,
|
|
97
|
+
responses: {
|
|
98
|
+
200: { description: "Matching scripts" },
|
|
99
|
+
400: { description: "Validation error" },
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const deleteRoute = route({
|
|
104
|
+
method: "delete",
|
|
105
|
+
path: "/api/scripts/{name}",
|
|
106
|
+
pattern: ["api", "scripts", null],
|
|
107
|
+
operationId: "scripts_delete",
|
|
108
|
+
summary: "Delete a reusable script",
|
|
109
|
+
tags: ["Scripts"],
|
|
110
|
+
params: nameParamsSchema,
|
|
111
|
+
query: scopeQuerySchema,
|
|
112
|
+
responses: {
|
|
113
|
+
200: { description: "Delete result" },
|
|
114
|
+
400: { description: "Validation error" },
|
|
115
|
+
403: { description: "Global delete requires lead agent" },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const typesRoute = route({
|
|
120
|
+
method: "get",
|
|
121
|
+
path: "/api/scripts/{name}/types",
|
|
122
|
+
pattern: ["api", "scripts", null, "types"],
|
|
123
|
+
operationId: "scripts_types",
|
|
124
|
+
summary: "Get script signature and authoring types",
|
|
125
|
+
tags: ["Scripts"],
|
|
126
|
+
params: nameParamsSchema,
|
|
127
|
+
query: optionalScopeQuerySchema,
|
|
128
|
+
responses: {
|
|
129
|
+
200: { description: "Script signature and type blobs" },
|
|
130
|
+
404: { description: "Script not found" },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function requireAgent(res: ServerResponse, agentId: string | undefined) {
|
|
135
|
+
if (!agentId) {
|
|
136
|
+
jsonError(res, "X-Agent-ID required for scripts API", 400);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const agent = getAgentById(agentId);
|
|
140
|
+
if (!agent) {
|
|
141
|
+
jsonError(res, "Agent not found", 404);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return agent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function signatureJsonFor(source: string): string {
|
|
148
|
+
return JSON.stringify(extractScriptSignature(source));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveScript(name: string, agentId: string, scope?: ScriptScope): ScriptRecord | null {
|
|
152
|
+
if (scope === "global") return getScript({ name, scope: "global" });
|
|
153
|
+
if (scope === "agent") return getScript({ name, scope: "agent", scopeId: agentId });
|
|
154
|
+
return (
|
|
155
|
+
getScript({ name, scope: "agent", scopeId: agentId }) ?? getScript({ name, scope: "global" })
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function scratchSlug(intent: string, source: string): string {
|
|
160
|
+
const base = (intent || "inline-script")
|
|
161
|
+
.toLowerCase()
|
|
162
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
163
|
+
.replace(/^-+|-+$/g, "")
|
|
164
|
+
.slice(0, 48);
|
|
165
|
+
const hash = new Bun.CryptoHasher("sha256").update(source).digest("hex").slice(0, 8);
|
|
166
|
+
return `scratch-${base || "inline-script"}-${hash}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function emitGlobalUpsertEvent(args: {
|
|
170
|
+
agentId: string;
|
|
171
|
+
script: ScriptRecord;
|
|
172
|
+
isNew: boolean;
|
|
173
|
+
isPromotion: boolean;
|
|
174
|
+
}) {
|
|
175
|
+
createEvent({
|
|
176
|
+
category: "system",
|
|
177
|
+
event: "script.global_upsert",
|
|
178
|
+
source: "api",
|
|
179
|
+
agentId: args.agentId,
|
|
180
|
+
data: {
|
|
181
|
+
scriptId: args.script.id,
|
|
182
|
+
name: args.script.name,
|
|
183
|
+
version: args.script.version,
|
|
184
|
+
contentHash: args.script.contentHash,
|
|
185
|
+
changedByAgentId: args.agentId,
|
|
186
|
+
isNew: args.isNew,
|
|
187
|
+
isPromotion: args.isPromotion,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function handleScripts(
|
|
193
|
+
req: IncomingMessage,
|
|
194
|
+
res: ServerResponse,
|
|
195
|
+
pathSegments: string[],
|
|
196
|
+
queryParams: URLSearchParams,
|
|
197
|
+
agentId: string | undefined,
|
|
198
|
+
): Promise<boolean> {
|
|
199
|
+
if (upsertRoute.match(req.method, pathSegments)) {
|
|
200
|
+
const parsed = await upsertRoute.parse(req, res, pathSegments, queryParams);
|
|
201
|
+
if (!parsed) return true;
|
|
202
|
+
const agent = requireAgent(res, agentId);
|
|
203
|
+
if (!agent) return true;
|
|
204
|
+
|
|
205
|
+
if (parsed.body.scope === "global" && !agent.isLead) {
|
|
206
|
+
jsonError(res, "Global scripts require a lead agent", 403);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const typecheck = typecheckScript(parsed.body.source);
|
|
211
|
+
if (!typecheck.ok) {
|
|
212
|
+
json(res, { error: "typecheck_failed", diagnostics: typecheck.diagnostics }, 400);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const existingAgentScript =
|
|
217
|
+
parsed.body.scope === "global"
|
|
218
|
+
? getScript({ name: parsed.body.name, scope: "agent", scopeId: agent.id })
|
|
219
|
+
: null;
|
|
220
|
+
const result = await upsertScriptByName({
|
|
221
|
+
name: parsed.body.name,
|
|
222
|
+
scope: parsed.body.scope,
|
|
223
|
+
scopeId: parsed.body.scope === "agent" ? agent.id : null,
|
|
224
|
+
source: parsed.body.source,
|
|
225
|
+
description: parsed.body.description,
|
|
226
|
+
intent: parsed.body.intent,
|
|
227
|
+
signatureJson: signatureJsonFor(parsed.body.source),
|
|
228
|
+
fsMode: parsed.body.fsMode,
|
|
229
|
+
agentId: agent.id,
|
|
230
|
+
isScratch: false,
|
|
231
|
+
typeChecked: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (parsed.body.scope === "global" && !result.contentDeduped) {
|
|
235
|
+
emitGlobalUpsertEvent({
|
|
236
|
+
agentId: agent.id,
|
|
237
|
+
script: result.script,
|
|
238
|
+
isNew: result.isNew,
|
|
239
|
+
isPromotion: Boolean(existingAgentScript),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
json(res, {
|
|
244
|
+
name: result.script.name,
|
|
245
|
+
version: result.script.version,
|
|
246
|
+
contentDeduped: result.contentDeduped,
|
|
247
|
+
});
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (runRoute.match(req.method, pathSegments)) {
|
|
252
|
+
const parsed = await runRoute.parse(req, res, pathSegments, queryParams);
|
|
253
|
+
if (!parsed) return true;
|
|
254
|
+
const agent = requireAgent(res, agentId);
|
|
255
|
+
if (!agent) return true;
|
|
256
|
+
|
|
257
|
+
let source = parsed.body.source;
|
|
258
|
+
let fsMode = parsed.body.fsMode;
|
|
259
|
+
if (parsed.body.name) {
|
|
260
|
+
const script = resolveScript(parsed.body.name, agent.id, parsed.body.scope);
|
|
261
|
+
if (!script) {
|
|
262
|
+
jsonError(res, "Script not found", 404);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
source = script.source;
|
|
266
|
+
fsMode = script.fsMode;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (fsMode === "workspace-rw") {
|
|
270
|
+
jsonError(res, "workspace-rw scripts are not supported by /api/scripts/run in v1", 501);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const output = await runScript({
|
|
275
|
+
source: source as string,
|
|
276
|
+
args: parsed.body.args,
|
|
277
|
+
fsMode,
|
|
278
|
+
agentId: agent.id,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let autoSaved: { slug: string; reason: string } | undefined;
|
|
282
|
+
if (parsed.body.source && !output.error && output.exitCode === 0) {
|
|
283
|
+
const slug = scratchSlug(parsed.body.intent, parsed.body.source);
|
|
284
|
+
await upsertScriptByName({
|
|
285
|
+
name: slug,
|
|
286
|
+
scope: "agent",
|
|
287
|
+
scopeId: agent.id,
|
|
288
|
+
source: parsed.body.source,
|
|
289
|
+
description: `Scratch script: ${parsed.body.intent || slug}`,
|
|
290
|
+
intent: parsed.body.intent || "Inline script auto-saved after successful run",
|
|
291
|
+
signatureJson: signatureJsonFor(parsed.body.source),
|
|
292
|
+
fsMode: "none",
|
|
293
|
+
agentId: agent.id,
|
|
294
|
+
isScratch: true,
|
|
295
|
+
typeChecked: false,
|
|
296
|
+
changeReason: "Auto-saved successful inline run",
|
|
297
|
+
});
|
|
298
|
+
autoSaved = { slug, reason: "successful_inline_run" };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
json(
|
|
302
|
+
res,
|
|
303
|
+
scrubObject({
|
|
304
|
+
result: output.result,
|
|
305
|
+
autoSaved,
|
|
306
|
+
truncated: output.truncated,
|
|
307
|
+
durationMs: output.durationMs,
|
|
308
|
+
stdout: output.stdout,
|
|
309
|
+
stderr: output.stderr,
|
|
310
|
+
exitCode: output.exitCode,
|
|
311
|
+
error: output.error,
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (searchRoute.match(req.method, pathSegments)) {
|
|
318
|
+
const parsed = await searchRoute.parse(req, res, pathSegments, queryParams);
|
|
319
|
+
if (!parsed) return true;
|
|
320
|
+
const agent = requireAgent(res, agentId);
|
|
321
|
+
if (!agent) return true;
|
|
322
|
+
|
|
323
|
+
const matches = await searchScripts({
|
|
324
|
+
query: parsed.body.query,
|
|
325
|
+
scope: parsed.body.scope,
|
|
326
|
+
scopeId: agent.id,
|
|
327
|
+
limit: parsed.body.limit,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
json(res, {
|
|
331
|
+
results: matches.map(({ script, score }) => ({
|
|
332
|
+
name: script.name,
|
|
333
|
+
signature: JSON.parse(script.signatureJson),
|
|
334
|
+
description: script.description,
|
|
335
|
+
score,
|
|
336
|
+
})),
|
|
337
|
+
});
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (typesRoute.match(req.method, pathSegments)) {
|
|
342
|
+
const parsed = await typesRoute.parse(req, res, pathSegments, queryParams);
|
|
343
|
+
if (!parsed) return true;
|
|
344
|
+
const agent = requireAgent(res, agentId);
|
|
345
|
+
if (!agent) return true;
|
|
346
|
+
|
|
347
|
+
const script = resolveScript(parsed.params.name, agent.id, parsed.query.scope);
|
|
348
|
+
if (!script) {
|
|
349
|
+
jsonError(res, "Script not found", 404);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
json(res, {
|
|
353
|
+
signature: JSON.parse(script.signatureJson),
|
|
354
|
+
sdkTypes: SCRIPT_SDK_TYPES,
|
|
355
|
+
stdlibTypes: SCRIPT_STDLIB_TYPES,
|
|
356
|
+
});
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (deleteRoute.match(req.method, pathSegments)) {
|
|
361
|
+
const parsed = await deleteRoute.parse(req, res, pathSegments, queryParams);
|
|
362
|
+
if (!parsed) return true;
|
|
363
|
+
const agent = requireAgent(res, agentId);
|
|
364
|
+
if (!agent) return true;
|
|
365
|
+
|
|
366
|
+
if (parsed.query.scope === "global" && !agent.isLead) {
|
|
367
|
+
jsonError(res, "Global scripts require a lead agent", 403);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const deleted = deleteScript({
|
|
372
|
+
name: parsed.params.name,
|
|
373
|
+
scope: parsed.query.scope,
|
|
374
|
+
scopeId: parsed.query.scope === "agent" ? agent.id : null,
|
|
375
|
+
});
|
|
376
|
+
json(res, { deleted });
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return false;
|
|
381
|
+
}
|
package/src/http/session-data.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getSessionLogsByTaskId,
|
|
14
14
|
getTaskById,
|
|
15
15
|
} from "../be/db";
|
|
16
|
+
import { normalizeModelKey } from "../be/pricing-normalize";
|
|
16
17
|
import type { SessionCost, SessionCostSource } from "../types";
|
|
17
18
|
import { route } from "./route-def";
|
|
18
19
|
import { json, jsonError } from "./utils";
|
|
@@ -65,17 +66,24 @@ const createSessionCostRoute = route({
|
|
|
65
66
|
inputTokens: z.number().int().optional(),
|
|
66
67
|
outputTokens: z.number().int().optional(),
|
|
67
68
|
cacheReadTokens: z.number().int().optional(),
|
|
68
|
-
|
|
69
|
+
// Migration 063: nullable — adapters that can't honestly report cache writes
|
|
70
|
+
// (e.g. Codex SDK) prefer null over a faked 0.
|
|
71
|
+
cacheWriteTokens: z.number().int().nullable().optional(),
|
|
72
|
+
// Migration 063: new token classes previously dropped on the floor.
|
|
73
|
+
reasoningOutputTokens: z.number().int().nonnegative().optional(),
|
|
74
|
+
thinkingTokens: z.number().int().nonnegative().optional(),
|
|
69
75
|
durationMs: z.number().int().optional(),
|
|
70
|
-
|
|
76
|
+
// Migration 063: nullable for adapters that can't honestly report numTurns.
|
|
77
|
+
numTurns: z.number().int().nullable().optional(),
|
|
71
78
|
model: z.string().optional(),
|
|
72
79
|
isError: z.boolean().optional(),
|
|
73
80
|
/**
|
|
74
|
-
* Phase 6
|
|
75
|
-
*
|
|
76
|
-
* Optional / undefined keeps back-compat for existing callers.
|
|
81
|
+
* Phase 6 (extended migration 063): drives the API recompute path. After
|
|
82
|
+
* Phase 2 every provider with seeded pricing rows participates.
|
|
77
83
|
*/
|
|
78
|
-
provider: z
|
|
84
|
+
provider: z
|
|
85
|
+
.enum(["claude", "claude-managed", "codex", "pi", "opencode", "devin", "gemini"])
|
|
86
|
+
.optional(),
|
|
79
87
|
/**
|
|
80
88
|
* Phase 6: epoch-ms timestamp used as the "active price at time T" lookup
|
|
81
89
|
* basis. Defaults to `Date.now()` when omitted. Including it lets
|
|
@@ -185,35 +193,75 @@ export async function handleSessionData(
|
|
|
185
193
|
try {
|
|
186
194
|
const inputTokens = parsed.body.inputTokens ?? 0;
|
|
187
195
|
const cachedInputTokens = parsed.body.cacheReadTokens ?? 0;
|
|
196
|
+
const cacheWriteTokens = parsed.body.cacheWriteTokens ?? 0;
|
|
188
197
|
const outputTokens = parsed.body.outputTokens ?? 0;
|
|
189
|
-
|
|
198
|
+
// Phase 2: don't paper over a missing model with a fake default — that
|
|
199
|
+
// poisoned the pricing-table lookup against the wrong rate. Only the
|
|
200
|
+
// back-compat case (no provider tag) keeps "opus" so old callers don't
|
|
201
|
+
// explode.
|
|
202
|
+
const model = parsed.body.model || (parsed.body.provider ? "" : "opus");
|
|
190
203
|
|
|
191
|
-
// Phase
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
204
|
+
// Phase 2: widen the recompute branch beyond codex. For any provider
|
|
205
|
+
// with a known model and seeded pricing rows, recompute `totalCostUsd`
|
|
206
|
+
// from tokens × DB prices and tag the row 'pricing-table'. When the
|
|
207
|
+
// (provider, model) pair has no pricing rows at all, tag 'unpriced' so
|
|
208
|
+
// the UI can flag it. When the provider isn't set, fall through with
|
|
209
|
+
// 'harness' (back-compat for older callers).
|
|
197
210
|
let totalCostUsd = parsed.body.totalCostUsd;
|
|
198
211
|
let costSource: SessionCostSource = "harness";
|
|
199
212
|
|
|
200
|
-
if (parsed.body.provider
|
|
213
|
+
if (parsed.body.provider && model) {
|
|
201
214
|
const lookupTime = parsed.body.createdAt ?? Date.now();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
// Phase 2 fix — different harnesses prepend routing prefixes
|
|
216
|
+
// (`openrouter/`, `github-copilot/`, …) to the same underlying model
|
|
217
|
+
// id. The pricing seed stores canonical (un-prefixed) keys, so we
|
|
218
|
+
// strip the prefix here before lookup. The original adapter-emitted
|
|
219
|
+
// string is still persisted to `session_costs.model` for debugging.
|
|
220
|
+
const lookupModel = normalizeModelKey(parsed.body.provider, model);
|
|
221
|
+
const inputRow = getActivePricingRow(
|
|
222
|
+
parsed.body.provider,
|
|
223
|
+
lookupModel,
|
|
224
|
+
"input",
|
|
225
|
+
lookupTime,
|
|
226
|
+
);
|
|
227
|
+
const cachedRow = getActivePricingRow(
|
|
228
|
+
parsed.body.provider,
|
|
229
|
+
lookupModel,
|
|
230
|
+
"cached_input",
|
|
231
|
+
lookupTime,
|
|
232
|
+
);
|
|
233
|
+
const outputRow = getActivePricingRow(
|
|
234
|
+
parsed.body.provider,
|
|
235
|
+
lookupModel,
|
|
236
|
+
"output",
|
|
237
|
+
lookupTime,
|
|
238
|
+
);
|
|
239
|
+
const cacheWriteRow = getActivePricingRow(
|
|
240
|
+
parsed.body.provider,
|
|
241
|
+
lookupModel,
|
|
242
|
+
"cache_write",
|
|
243
|
+
lookupTime,
|
|
244
|
+
);
|
|
205
245
|
|
|
206
|
-
if (inputRow &&
|
|
207
|
-
// Mirror the
|
|
208
|
-
//
|
|
209
|
-
//
|
|
246
|
+
if (inputRow && outputRow) {
|
|
247
|
+
// Mirror the legacy codex semantic: uncached input is billed at the
|
|
248
|
+
// full rate, cached input at the discounted rate. Cache writes are
|
|
249
|
+
// billed separately when the provider's pricing table carries that
|
|
250
|
+
// class (anthropic) and the adapter reports a non-zero value.
|
|
210
251
|
const uncachedInputTokens = Math.max(0, inputTokens - cachedInputTokens);
|
|
252
|
+
const cachedRate = cachedRow?.pricePerMillionUsd ?? 0;
|
|
253
|
+
const cacheWriteRate = cacheWriteRow?.pricePerMillionUsd ?? 0;
|
|
211
254
|
totalCostUsd =
|
|
212
255
|
(uncachedInputTokens * inputRow.pricePerMillionUsd +
|
|
213
|
-
cachedInputTokens *
|
|
256
|
+
cachedInputTokens * cachedRate +
|
|
257
|
+
cacheWriteTokens * cacheWriteRate +
|
|
214
258
|
outputTokens * outputRow.pricePerMillionUsd) /
|
|
215
259
|
1_000_000;
|
|
216
260
|
costSource = "pricing-table";
|
|
261
|
+
} else {
|
|
262
|
+
// Provider was tagged but we have no pricing rows for it; flag the
|
|
263
|
+
// row so the UI can show an "unpriced" badge instead of pretending.
|
|
264
|
+
costSource = "unpriced";
|
|
217
265
|
}
|
|
218
266
|
}
|
|
219
267
|
|
|
@@ -226,8 +274,11 @@ export async function handleSessionData(
|
|
|
226
274
|
outputTokens,
|
|
227
275
|
cacheReadTokens: cachedInputTokens,
|
|
228
276
|
cacheWriteTokens: parsed.body.cacheWriteTokens ?? 0,
|
|
277
|
+
reasoningOutputTokens: parsed.body.reasoningOutputTokens ?? 0,
|
|
278
|
+
thinkingTokens: parsed.body.thinkingTokens ?? 0,
|
|
229
279
|
durationMs: parsed.body.durationMs ?? 0,
|
|
230
|
-
|
|
280
|
+
// Migration 063: pass null through honestly instead of faking a 1.
|
|
281
|
+
numTurns: parsed.body.numTurns ?? null,
|
|
231
282
|
model,
|
|
232
283
|
isError: parsed.body.isError ?? false,
|
|
233
284
|
costSource,
|
package/src/linear/outbound.ts
CHANGED
|
@@ -49,6 +49,10 @@ async function handleTaskCreated(data: unknown): Promise<void> {
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Cap parameter length to avoid oversized Linear GraphQL payloads. Linear renders this in the
|
|
53
|
+
// AgentSession panel; 2000 chars is plenty for a progress update.
|
|
54
|
+
const PROGRESS_PARAMETER_MAX = 2000;
|
|
55
|
+
|
|
52
56
|
async function handleTaskProgress(data: unknown): Promise<void> {
|
|
53
57
|
const { taskId, progress } = data as { taskId: string; progress?: string };
|
|
54
58
|
if (!taskId || !progress) return;
|
|
@@ -56,8 +60,11 @@ async function handleTaskProgress(data: unknown): Promise<void> {
|
|
|
56
60
|
const sessionId = taskSessionMap.get(taskId);
|
|
57
61
|
if (!sessionId) return;
|
|
58
62
|
|
|
59
|
-
//
|
|
60
|
-
|
|
63
|
+
// Post as `action` activity (renders as a structured card in Linear's AgentSession panel).
|
|
64
|
+
// Per Linear's agentActivityCreate spec, `action` requires BOTH `action` AND `parameter`;
|
|
65
|
+
// the original bug here was passing `progress` as `action` with `parameter` undefined.
|
|
66
|
+
const parameter = progress.slice(0, PROGRESS_PARAMETER_MAX);
|
|
67
|
+
postAgentSessionAction(sessionId, "Progress update", parameter).catch((err) => {
|
|
61
68
|
console.error(`[Linear Outbound] Failed to post progress action for task ${taskId}:`, err);
|
|
62
69
|
});
|
|
63
70
|
}
|