@desplega.ai/agent-swarm 1.145.0 → 1.147.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/README.md +2 -1
- package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
- package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
- package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
- package/dist/assistant-kt31dj2h.js +236 -0
- package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
- package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
- package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
- package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
- package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
- package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
- package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
- package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
- package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
- package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
- package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
- package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
- package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
- package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
- package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
- package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
- package/dist/cli-9d8ntetj.js +42 -0
- package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
- package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
- package/dist/cli-d8brzxjb.js +190 -0
- package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
- package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
- package/dist/cli-ep19k3mb.js +19 -0
- package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
- package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
- package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
- package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
- package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
- package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
- package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
- package/dist/cli-m9sxbkhm.js +779 -0
- package/dist/cli-mmemxxdc.js +823 -0
- package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
- package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
- package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
- package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
- package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
- package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
- package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
- package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
- package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
- package/dist/cli.js +13 -13
- package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
- package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
- package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
- package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
- package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
- package/dist/handlers-0aspcejm.js +603 -0
- package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
- package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
- package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
- package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
- package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
- package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
- package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
- package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
- package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
- package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
- package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
- package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
- package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
- package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
- package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
- package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
- package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
- package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
- package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
- package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
- package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
- package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
- package/openapi.json +263 -12
- package/package.json +1 -1
- package/plugin/commands/start-worker.md +4 -9
- package/plugin/pi-skills/start-worker/SKILL.md +4 -9
- package/src/be/db/agents.ts +556 -0
- package/src/be/db/context-versions.ts +117 -0
- package/src/be/db/runtime.ts +406 -0
- package/src/be/db/tasks/read.ts +1167 -0
- package/src/be/db/tasks/write.ts +953 -0
- package/src/be/db.ts +584 -3429
- package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
- package/src/be/migrations/148_pages_svg.sql +123 -0
- package/src/be/migrations/149_session_tokens.sql +24 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +13 -8
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
- package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
- package/src/be/seed-scripts/index.ts +17 -0
- package/src/be/seed-skills/index.ts +14 -0
- package/src/be/swarm-config-guard.ts +5 -2
- package/src/be/task-attachment-store.ts +70 -0
- package/src/be/users.ts +77 -0
- package/src/commands/attachments-section.ts +37 -0
- package/src/commands/claude-managed-setup.ts +45 -3
- package/src/commands/claude-md-session.ts +408 -0
- package/src/commands/context-preamble.ts +15 -3
- package/src/commands/profile-sync.ts +205 -5
- package/src/commands/provider-credentials.ts +60 -27
- package/src/commands/runner.ts +63 -58
- package/src/commands/templates.ts +25 -2
- package/src/hooks/hook.ts +26 -67
- package/src/http/agents.ts +40 -1
- package/src/http/auth.ts +13 -1
- package/src/http/fs.ts +21 -51
- package/src/http/index.ts +2 -0
- package/src/http/mcp.ts +32 -0
- package/src/http/pages-public.ts +20 -4
- package/src/http/pages.ts +11 -3
- package/src/http/poll.ts +4 -0
- package/src/http/repos.ts +3 -3
- package/src/http/sessions.ts +82 -0
- package/src/http/tasks.ts +10 -2
- package/src/http/utils.ts +79 -1
- package/src/prompts/session-templates.ts +12 -8
- package/src/providers/acp-adapter.ts +62 -8
- package/src/providers/pi-mono-adapter.ts +23 -6
- package/src/providers/pricing-sources.md +11 -0
- package/src/scheduler/scheduler.test.ts +49 -2
- package/src/scheduler/scheduler.ts +4 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +87 -46
- package/src/slack/handlers.test.ts +1 -3
- package/src/slack/handlers.ts +102 -87
- package/src/slack/inbound-files.ts +466 -0
- package/src/slack/templates.ts +14 -0
- package/src/tasks/task-terminal-effects.ts +158 -0
- package/src/tests/acp-adapter.test.ts +369 -117
- package/src/tests/budget-claim-gate.test.ts +1 -0
- package/src/tests/build-pi-skills.test.ts +5 -7
- package/src/tests/claude-managed-setup.test.ts +52 -4
- package/src/tests/claude-md-session.test.ts +603 -0
- package/src/tests/codex-skill-resolver.test.ts +19 -0
- package/src/tests/context-versioning.test.ts +107 -0
- package/src/tests/cors-allowlist.test.ts +309 -0
- package/src/tests/credential-check.test.ts +157 -14
- package/src/tests/db-agents.test.ts +91 -0
- package/src/tests/db-context-versions.test.ts +37 -0
- package/src/tests/db-extraction-contract.test.ts +45 -0
- package/src/tests/db-runtime.test.ts +70 -0
- package/src/tests/db-task-read-contract.test.ts +624 -0
- package/src/tests/db-task-write-contract.test.ts +653 -0
- package/src/tests/db-tasks-read.test.ts +183 -0
- package/src/tests/db-tasks-write.test.ts +230 -0
- package/src/tests/defer-task.test.ts +389 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/e2e-visuals-stage.test.ts +151 -0
- package/src/tests/file-lock.test.ts +144 -0
- package/src/tests/fixtures/file-lock-holder.ts +13 -0
- package/src/tests/fixtures/hold-file-lock.ts +25 -0
- package/src/tests/heartbeat-expiry.test.ts +188 -0
- package/src/tests/http-api-integration.test.ts +78 -1
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-svg-migration.test.ts +42 -0
- package/src/tests/preload.ts +4 -1
- package/src/tests/profile-sync-warning.test.ts +16 -0
- package/src/tests/profile-sync.test.ts +126 -0
- package/src/tests/prompt-template-session.test.ts +20 -4
- package/src/tests/provider-registration.test.ts +93 -0
- package/src/tests/rbac-e2e-helpers.test.ts +53 -0
- package/src/tests/rbac-e2e-helpers.ts +19 -2
- package/src/tests/reload-config.test.ts +26 -0
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/runner-attachments-section.test.ts +22 -2
- package/src/tests/runner-context-preamble.test.ts +30 -0
- package/src/tests/runner-deployment-resume.test.ts +107 -0
- package/src/tests/runner-repo-autostash.test.ts +12 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/session-tokens.test.ts +279 -0
- package/src/tests/slack-file-tools-attachments.test.ts +408 -0
- package/src/tests/slack-inbound-files.test.ts +847 -0
- package/src/tests/star-history-refresh.test.ts +102 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
- package/src/tests/swarm-repos.test.ts +20 -0
- package/src/tests/tool-annotations.test.ts +4 -3
- package/src/tests/ui-logs-parser.test.ts +176 -0
- package/src/tools/create-page.ts +4 -2
- package/src/tools/defer-task.ts +239 -0
- package/src/tools/repos/update-repo.ts +2 -2
- package/src/tools/schedules/create-schedule.ts +1 -0
- package/src/tools/schedules/list-schedules.ts +1 -0
- package/src/tools/schedules/patch-schedule.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +1 -0
- package/src/tools/slack-download-file.ts +60 -29
- package/src/tools/slack-read.ts +73 -32
- package/src/tools/store-progress.ts +16 -146
- package/src/tools/tool-config.ts +1 -0
- package/src/tools/utils.ts +21 -0
- package/src/types.ts +37 -4
- package/src/utils/acp-session-token.ts +56 -0
- package/src/utils/file-lock.ts +111 -0
- package/src/utils/heartbeat-expiry.ts +78 -0
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/task-attachment-links.ts +22 -0
- package/templates/skills/code-quality/content.md +8 -5
- package/templates/skills/download-task-attachment/content.md +2 -1
- package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
- package/templates/skills/review-offered-task/config.json +14 -0
- package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
- package/templates/skills/scheduling/content.md +4 -0
- package/templates/skills/slack-interaction/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +16 -0
- package/templates/skills/work-on-task/config.json +14 -0
- package/templates/skills/work-on-task/content.md +11 -0
- package/dist/assistant-0qt42nzg.js +0 -212
- package/dist/cli-fs1kkqgr.js +0 -317
- package/dist/cli-jnf5zghe.js +0 -269
- package/dist/cli-n1q8q14d.js +0 -177
- package/dist/handlers-yfp1qn81.js +0 -592
- package/plugin/commands/work-on-task.md +0 -39
- package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
6
|
+
import type { Usage } from "@agentclientprotocol/sdk";
|
|
5
7
|
import { normalizeSessionLogs } from "../../apps/ui/src/logs-parser";
|
|
6
8
|
import {
|
|
7
9
|
closeDb,
|
|
10
|
+
createAgent,
|
|
8
11
|
createSessionLogs,
|
|
9
12
|
createTaskExtended,
|
|
10
13
|
getSessionLogsByTaskId,
|
|
11
14
|
initDb,
|
|
12
15
|
} from "../be/db";
|
|
16
|
+
import { handleSessionData } from "../http/session-data";
|
|
17
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
13
18
|
import { createProviderAdapter } from "../providers";
|
|
14
19
|
import {
|
|
15
20
|
ACPAdapter,
|
|
@@ -19,6 +24,7 @@ import {
|
|
|
19
24
|
} from "../providers/acp-adapter";
|
|
20
25
|
import { AcpTargetResolutionError, resolveAcpTarget } from "../providers/acp-targets";
|
|
21
26
|
import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
|
|
27
|
+
import { listenOnFreePort } from "./test-net";
|
|
22
28
|
|
|
23
29
|
const tmpDirs: string[] = [];
|
|
24
30
|
|
|
@@ -51,6 +57,32 @@ function baseConfig(overrides: Partial<ProviderSessionConfig> = {}): ProviderSes
|
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Start a minimal swarm API stub that handles POST /api/sessions/tokens and
|
|
62
|
+
* DELETE /api/sessions/tokens/:id. Returns the server and its base URL.
|
|
63
|
+
* The caller is responsible for closing the server.
|
|
64
|
+
*/
|
|
65
|
+
async function startTokenStubServer(
|
|
66
|
+
tokenId: string,
|
|
67
|
+
plaintext: string,
|
|
68
|
+
): Promise<{ server: Server; apiUrl: string }> {
|
|
69
|
+
const stub = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
70
|
+
if (req.method === "POST" && req.url === "/api/sessions/tokens") {
|
|
71
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
72
|
+
res.end(JSON.stringify({ tokenId, plaintext }));
|
|
73
|
+
} else if (req.method === "DELETE" && req.url?.startsWith("/api/sessions/tokens/")) {
|
|
74
|
+
res.writeHead(204);
|
|
75
|
+
res.end();
|
|
76
|
+
} else {
|
|
77
|
+
res.writeHead(404);
|
|
78
|
+
res.end();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
await new Promise<void>((resolve) => stub.listen(0, "127.0.0.1", resolve));
|
|
82
|
+
const addr = stub.address() as import("net").AddressInfo;
|
|
83
|
+
return { server: stub, apiUrl: `http://127.0.0.1:${addr.port}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
describe("ACPAdapter", () => {
|
|
55
87
|
test("is registered with MCP and local-environment traits", async () => {
|
|
56
88
|
const adapter = await createProviderAdapter("acp");
|
|
@@ -68,7 +100,32 @@ describe("ACPAdapter", () => {
|
|
|
68
100
|
);
|
|
69
101
|
});
|
|
70
102
|
|
|
71
|
-
test
|
|
103
|
+
test.each<{ name: string; usage?: Usage | null }>([
|
|
104
|
+
{ name: "absent", usage: undefined },
|
|
105
|
+
{ name: "null", usage: null },
|
|
106
|
+
{
|
|
107
|
+
name: "OpenCode observed",
|
|
108
|
+
usage: {
|
|
109
|
+
inputTokens: 84376,
|
|
110
|
+
outputTokens: 65,
|
|
111
|
+
totalTokens: 86233,
|
|
112
|
+
cachedReadTokens: 1792,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "populated",
|
|
117
|
+
usage: {
|
|
118
|
+
totalTokens: 180,
|
|
119
|
+
inputTokens: 100,
|
|
120
|
+
outputTokens: 30,
|
|
121
|
+
thoughtTokens: 10,
|
|
122
|
+
cachedReadTokens: 25,
|
|
123
|
+
cachedWriteTokens: 15,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
])("persists $name ACP usage with deliberate zero-coalescing of missing session-cost counters", async ({
|
|
127
|
+
usage,
|
|
128
|
+
}) => {
|
|
72
129
|
const cwd = makeTempDir();
|
|
73
130
|
const agentPath = join(cwd, "fake-acp-agent.ts");
|
|
74
131
|
const sdkPath = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
|
|
@@ -222,7 +279,15 @@ class FakeAgent {
|
|
|
222
279
|
rawOutput: { chunks: Array.from({ length: 20 }, () => "z".repeat(2_000)) },
|
|
223
280
|
},
|
|
224
281
|
});
|
|
225
|
-
return {
|
|
282
|
+
return ${JSON.stringify({
|
|
283
|
+
stopReason: "end_turn",
|
|
284
|
+
usage,
|
|
285
|
+
_meta: {
|
|
286
|
+
debug: "ghp_abcdefghijklmnopqrstuvwxyz0123456789",
|
|
287
|
+
authorization: "opaque-response-credential",
|
|
288
|
+
note: "response metadata",
|
|
289
|
+
},
|
|
290
|
+
})};
|
|
226
291
|
}
|
|
227
292
|
|
|
228
293
|
async cancel() {}
|
|
@@ -235,133 +300,205 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
235
300
|
`,
|
|
236
301
|
);
|
|
237
302
|
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
cwd,
|
|
242
|
-
env: {
|
|
243
|
-
PATH: process.env.PATH ?? "",
|
|
244
|
-
HOME: process.env.HOME ?? "",
|
|
245
|
-
ACP_TARGET_COMMAND: "bun",
|
|
246
|
-
ACP_TARGET_ARGS: JSON.stringify([agentPath]),
|
|
247
|
-
ACP_CONFIG_OPTIONS: JSON.stringify({ thought: "high" }),
|
|
248
|
-
},
|
|
249
|
-
}),
|
|
303
|
+
const { server: tokenStub, apiUrl } = await startTokenStubServer(
|
|
304
|
+
"stub-token-id",
|
|
305
|
+
"aseph_stubtokenfortest1234567890",
|
|
250
306
|
);
|
|
251
307
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
308
|
+
try {
|
|
309
|
+
const adapter = new ACPAdapter();
|
|
310
|
+
const session = await adapter.createSession(
|
|
311
|
+
baseConfig({
|
|
312
|
+
cwd,
|
|
313
|
+
apiUrl,
|
|
314
|
+
env: {
|
|
315
|
+
PATH: process.env.PATH ?? "",
|
|
316
|
+
HOME: process.env.HOME ?? "",
|
|
317
|
+
ACP_TARGET_COMMAND: "bun",
|
|
318
|
+
ACP_TARGET_ARGS: JSON.stringify([agentPath]),
|
|
319
|
+
ACP_CONFIG_OPTIONS: JSON.stringify({ thought: "high" }),
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
255
323
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
output: "done",
|
|
260
|
-
isError: false,
|
|
261
|
-
});
|
|
262
|
-
expect(events.some((event) => event.type === "session_init")).toBe(true);
|
|
263
|
-
expect(events.find((event) => event.type === "session_init")).toMatchObject({
|
|
264
|
-
providerMeta: {
|
|
265
|
-
target: "custom",
|
|
266
|
-
configOptions: [
|
|
267
|
-
{ id: "model", currentValue: "fake-model" },
|
|
268
|
-
{ id: "thought", currentValue: "high" },
|
|
269
|
-
],
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
expect(events.some((event) => event.type === "message" && event.content === "done")).toBe(true);
|
|
273
|
-
expect(events.some((event) => event.type === "tool_start")).toBe(true);
|
|
274
|
-
expect(events.some((event) => event.type === "tool_end")).toBe(true);
|
|
275
|
-
expect(events.some((event) => event.type === "result")).toBe(true);
|
|
276
|
-
|
|
277
|
-
const rawLogs = events
|
|
278
|
-
.filter(
|
|
279
|
-
(event): event is Extract<ProviderEvent, { type: "raw_log" }> => event.type === "raw_log",
|
|
280
|
-
)
|
|
281
|
-
.map((event) => event.content);
|
|
282
|
-
expect(rawLogs.length).toBeGreaterThan(0);
|
|
283
|
-
expect(
|
|
284
|
-
rawLogs.some((content) => {
|
|
285
|
-
const event = JSON.parse(content) as Record<string, unknown>;
|
|
286
|
-
const update = event.update as Record<string, unknown> | undefined;
|
|
287
|
-
return update?.sessionUpdate === "agent_message_chunk";
|
|
288
|
-
}),
|
|
289
|
-
).toBe(true);
|
|
290
|
-
expect(
|
|
291
|
-
rawLogs.some((content) => {
|
|
292
|
-
const event = JSON.parse(content) as Record<string, unknown>;
|
|
293
|
-
const update = event.update as Record<string, unknown> | undefined;
|
|
294
|
-
return update?.sessionUpdate === "current_mode_update";
|
|
295
|
-
}),
|
|
296
|
-
).toBe(true);
|
|
297
|
-
expect(
|
|
298
|
-
rawLogs.some((content) => {
|
|
299
|
-
const event = JSON.parse(content) as Record<string, unknown>;
|
|
300
|
-
return event.type === "message" && event.content === "done";
|
|
301
|
-
}),
|
|
302
|
-
).toBe(true);
|
|
303
|
-
expect(rawLogs.every((content) => content.length <= 30_000)).toBe(true);
|
|
324
|
+
const events: ProviderEvent[] = [];
|
|
325
|
+
session.onEvent((event) => events.push(event));
|
|
326
|
+
const result = await session.waitForCompletion();
|
|
304
327
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
sessionId: session.sessionId,
|
|
311
|
-
iteration: 1,
|
|
312
|
-
cli: "acp",
|
|
313
|
-
lines: rawLogs,
|
|
328
|
+
expect(result).toMatchObject({
|
|
329
|
+
exitCode: 0,
|
|
330
|
+
sessionId: "acp-session-1",
|
|
331
|
+
output: "done",
|
|
332
|
+
isError: false,
|
|
314
333
|
});
|
|
315
|
-
|
|
316
|
-
expect(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
`opaque-map-${headerName}-credential`,
|
|
339
|
-
]);
|
|
340
|
-
for (const credentialValue of credentialValues) {
|
|
341
|
-
expect(persistedJson).not.toContain(credentialValue);
|
|
342
|
-
}
|
|
343
|
-
expect(persistedJson).toContain("X-Debug");
|
|
344
|
-
expect(persistedJson).toContain("X-Debug-Map");
|
|
345
|
-
expect(persistedJson).not.toContain("ghp_abcdefghijklmnopqrstuvwxyz0123456789");
|
|
346
|
-
expect(persistedJson).toContain("[REDACTED:github_token]");
|
|
347
|
-
expect(persistedJson).toContain("… [truncated]");
|
|
348
|
-
expect(persistedJson).not.toContain("x".repeat(30_001));
|
|
349
|
-
const transcript = normalizeSessionLogs(persisted);
|
|
350
|
-
expect(transcript.items.some((item) => item.kind === "unknown")).toBe(false);
|
|
334
|
+
expect(events.some((event) => event.type === "session_init")).toBe(true);
|
|
335
|
+
expect(events.find((event) => event.type === "session_init")).toMatchObject({
|
|
336
|
+
providerMeta: {
|
|
337
|
+
target: "custom",
|
|
338
|
+
configOptions: [
|
|
339
|
+
{ id: "model", currentValue: "fake-model" },
|
|
340
|
+
{ id: "thought", currentValue: "high" },
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
expect(events.some((event) => event.type === "message" && event.content === "done")).toBe(
|
|
345
|
+
true,
|
|
346
|
+
);
|
|
347
|
+
expect(events.some((event) => event.type === "tool_start")).toBe(true);
|
|
348
|
+
expect(events.some((event) => event.type === "tool_end")).toBe(true);
|
|
349
|
+
expect(events.some((event) => event.type === "result")).toBe(true);
|
|
350
|
+
|
|
351
|
+
const rawLogs = events
|
|
352
|
+
.filter(
|
|
353
|
+
(event): event is Extract<ProviderEvent, { type: "raw_log" }> => event.type === "raw_log",
|
|
354
|
+
)
|
|
355
|
+
.map((event) => event.content);
|
|
356
|
+
expect(rawLogs.length).toBeGreaterThan(0);
|
|
351
357
|
expect(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
358
|
+
rawLogs.some((content) => {
|
|
359
|
+
const event = JSON.parse(content) as Record<string, unknown>;
|
|
360
|
+
const update = event.update as Record<string, unknown> | undefined;
|
|
361
|
+
return update?.sessionUpdate === "agent_message_chunk";
|
|
362
|
+
}),
|
|
355
363
|
).toBe(true);
|
|
356
364
|
expect(
|
|
357
|
-
|
|
365
|
+
rawLogs.some((content) => {
|
|
366
|
+
const event = JSON.parse(content) as Record<string, unknown>;
|
|
367
|
+
const update = event.update as Record<string, unknown> | undefined;
|
|
368
|
+
return update?.sessionUpdate === "current_mode_update";
|
|
369
|
+
}),
|
|
358
370
|
).toBe(true);
|
|
359
371
|
expect(
|
|
360
|
-
|
|
361
|
-
|
|
372
|
+
rawLogs.some((content) => {
|
|
373
|
+
const event = JSON.parse(content) as Record<string, unknown>;
|
|
374
|
+
return event.type === "message" && event.content === "done";
|
|
375
|
+
}),
|
|
362
376
|
).toBe(true);
|
|
377
|
+
expect(rawLogs.every((content) => content.length <= 30_000)).toBe(true);
|
|
378
|
+
|
|
379
|
+
initDb(":memory:");
|
|
380
|
+
try {
|
|
381
|
+
const task = await createTaskExtended("ACP persistence test");
|
|
382
|
+
await createSessionLogs({
|
|
383
|
+
taskId: task.id,
|
|
384
|
+
sessionId: session.sessionId,
|
|
385
|
+
iteration: 1,
|
|
386
|
+
cli: "acp",
|
|
387
|
+
lines: rawLogs,
|
|
388
|
+
});
|
|
389
|
+
const persisted = await getSessionLogsByTaskId(task.id);
|
|
390
|
+
expect(persisted).toHaveLength(rawLogs.length);
|
|
391
|
+
expect(persisted.map((entry) => entry.content)).toEqual(rawLogs);
|
|
392
|
+
expect(persisted.every((entry) => entry.cli === "acp")).toBe(true);
|
|
393
|
+
// Match saveCostData's JSON transport: undefined counters disappear on the wire.
|
|
394
|
+
// The existing API deliberately coalesces them to zero; only raw logs retain absence.
|
|
395
|
+
const agent = await createAgent({ name: "ACP cost test", isLead: false, status: "idle" });
|
|
396
|
+
const server = createServer(async (req, res) => {
|
|
397
|
+
const handled = await handleSessionData(
|
|
398
|
+
req,
|
|
399
|
+
res,
|
|
400
|
+
getPathSegments(req.url ?? ""),
|
|
401
|
+
parseQueryParams(req.url ?? ""),
|
|
402
|
+
agent.id,
|
|
403
|
+
);
|
|
404
|
+
if (!handled) {
|
|
405
|
+
res.writeHead(404);
|
|
406
|
+
res.end();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
try {
|
|
410
|
+
const port = await listenOnFreePort(server);
|
|
411
|
+
const endpoint = `http://127.0.0.1:${port}/api/session-costs`;
|
|
412
|
+
const body = JSON.stringify({ ...result.cost, agentId: agent.id, taskId: task.id });
|
|
413
|
+
if (!usage) {
|
|
414
|
+
for (const counter of [
|
|
415
|
+
"inputTokens",
|
|
416
|
+
"outputTokens",
|
|
417
|
+
"cacheReadTokens",
|
|
418
|
+
"cacheWriteTokens",
|
|
419
|
+
]) {
|
|
420
|
+
expect(JSON.parse(body)).not.toHaveProperty(counter);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const response = await fetch(endpoint, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
headers: { "Content-Type": "application/json" },
|
|
426
|
+
body,
|
|
427
|
+
});
|
|
428
|
+
expect(response.status).toBe(201);
|
|
429
|
+
const { cost } = await response.json();
|
|
430
|
+
expect(cost).toMatchObject({
|
|
431
|
+
inputTokens: usage?.inputTokens ?? 0,
|
|
432
|
+
outputTokens: usage?.outputTokens ?? 0,
|
|
433
|
+
cacheReadTokens: usage?.cachedReadTokens ?? 0,
|
|
434
|
+
cacheWriteTokens: usage?.cachedWriteTokens ?? 0,
|
|
435
|
+
costSource: "unpriced",
|
|
436
|
+
});
|
|
437
|
+
const readback = await fetch(`${endpoint}?taskId=${task.id}`);
|
|
438
|
+
expect(readback.status).toBe(200);
|
|
439
|
+
const { costs } = await readback.json();
|
|
440
|
+
expect(costs).toHaveLength(1);
|
|
441
|
+
expect(costs[0]).toMatchObject({
|
|
442
|
+
id: cost.id,
|
|
443
|
+
inputTokens: cost.inputTokens,
|
|
444
|
+
outputTokens: cost.outputTokens,
|
|
445
|
+
cacheReadTokens: cost.cacheReadTokens,
|
|
446
|
+
cacheWriteTokens: cost.cacheWriteTokens,
|
|
447
|
+
costSource: cost.costSource,
|
|
448
|
+
});
|
|
449
|
+
} finally {
|
|
450
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
451
|
+
}
|
|
452
|
+
const persistedJson = persisted.map((entry) => entry.content).join("\n");
|
|
453
|
+
const credentialHeaderNames = [
|
|
454
|
+
"authorization",
|
|
455
|
+
"proxy-authorization",
|
|
456
|
+
"cookie",
|
|
457
|
+
"set-cookie",
|
|
458
|
+
"www-authenticate",
|
|
459
|
+
"proxy-authenticate",
|
|
460
|
+
"x-api-key",
|
|
461
|
+
"api-key",
|
|
462
|
+
"x-auth-token",
|
|
463
|
+
"x-access-token",
|
|
464
|
+
"x-session-token",
|
|
465
|
+
];
|
|
466
|
+
for (const headerName of credentialHeaderNames) {
|
|
467
|
+
expect(persistedJson.toLowerCase()).not.toContain(headerName);
|
|
468
|
+
}
|
|
469
|
+
const credentialValues = credentialHeaderNames.flatMap((headerName) => [
|
|
470
|
+
`opaque-array-${headerName}-credential`,
|
|
471
|
+
`opaque-map-${headerName}-credential`,
|
|
472
|
+
]);
|
|
473
|
+
for (const credentialValue of credentialValues) {
|
|
474
|
+
expect(persistedJson).not.toContain(credentialValue);
|
|
475
|
+
}
|
|
476
|
+
expect(persistedJson).toContain("X-Debug");
|
|
477
|
+
expect(persistedJson).toContain("X-Debug-Map");
|
|
478
|
+
expect(persistedJson).not.toContain("ghp_abcdefghijklmnopqrstuvwxyz0123456789");
|
|
479
|
+
expect(persistedJson).toContain("[REDACTED:github_token]");
|
|
480
|
+
expect(persistedJson).toContain("… [truncated]");
|
|
481
|
+
expect(persistedJson).not.toContain("x".repeat(30_001));
|
|
482
|
+
const transcript = normalizeSessionLogs(persisted);
|
|
483
|
+
expect(transcript.items.some((item) => item.kind === "unknown")).toBe(false);
|
|
484
|
+
expect(
|
|
485
|
+
transcript.items.some(
|
|
486
|
+
(item) => item.kind === "text" && item.role === "assistant" && item.text === "done",
|
|
487
|
+
),
|
|
488
|
+
).toBe(true);
|
|
489
|
+
expect(
|
|
490
|
+
transcript.items.some((item) => item.kind === "tool_call" && item.tool?.id === "tool-1"),
|
|
491
|
+
).toBe(true);
|
|
492
|
+
expect(
|
|
493
|
+
transcript.items.find(
|
|
494
|
+
(item) => item.kind === "tool_result" && item.result?.id === "tool-1",
|
|
495
|
+
)?.result?.isError,
|
|
496
|
+
).toBe(true);
|
|
497
|
+
} finally {
|
|
498
|
+
closeDb();
|
|
499
|
+
}
|
|
363
500
|
} finally {
|
|
364
|
-
|
|
501
|
+
await new Promise<void>((resolve) => tokenStub.close(() => resolve()));
|
|
365
502
|
}
|
|
366
503
|
});
|
|
367
504
|
|
|
@@ -550,4 +687,119 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
550
687
|
expect(toAcpMcpServers({ broken: { foo: "bar" } })).toEqual([]);
|
|
551
688
|
expect(toAcpMcpServers(null)).toEqual([]);
|
|
552
689
|
});
|
|
690
|
+
|
|
691
|
+
test("mints an ephemeral token and revokes it when the session ends", async () => {
|
|
692
|
+
const cwd = makeTempDir();
|
|
693
|
+
const agentPath = join(cwd, "fake-acp-ephem-agent.ts");
|
|
694
|
+
const sdkPath = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
|
|
695
|
+
|
|
696
|
+
const captureFile = join(cwd, "captured-auth.txt");
|
|
697
|
+
|
|
698
|
+
await Bun.write(
|
|
699
|
+
agentPath,
|
|
700
|
+
`
|
|
701
|
+
import { Readable, Writable } from "node:stream";
|
|
702
|
+
import { AgentSideConnection, PROTOCOL_VERSION, ndJsonStream } from "${sdkPath}";
|
|
703
|
+
class FakeAgent {
|
|
704
|
+
constructor(connection) { this.connection = connection; }
|
|
705
|
+
async initialize() {
|
|
706
|
+
return { protocolVersion: PROTOCOL_VERSION, agentCapabilities: { loadSession: false } };
|
|
707
|
+
}
|
|
708
|
+
async newSession(params) {
|
|
709
|
+
const swarm = params.mcpServers.find((s) => s.name === "swarm");
|
|
710
|
+
const auth = swarm?.headers?.find((h) => h.name === "Authorization")?.value ?? "";
|
|
711
|
+
// Write bearer to a file so the test can read it back without relying on
|
|
712
|
+
// a custom sessionUpdate type that the ACP SDK schema rejects.
|
|
713
|
+
await Bun.write(${JSON.stringify(captureFile)}, auth);
|
|
714
|
+
// newSession must return { sessionId, configOptions? }, not a prompt response.
|
|
715
|
+
return { sessionId: "ephem-session-1" };
|
|
716
|
+
}
|
|
717
|
+
async prompt(params) {
|
|
718
|
+
await this.connection.sessionUpdate({
|
|
719
|
+
sessionId: params.sessionId,
|
|
720
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "done" }, messageId: "m1" },
|
|
721
|
+
});
|
|
722
|
+
return { stopReason: "end_turn" };
|
|
723
|
+
}
|
|
724
|
+
async cancel() {}
|
|
725
|
+
}
|
|
726
|
+
const stream = ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin));
|
|
727
|
+
new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
728
|
+
`,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
// Minimal swarm API stub: tracks calls to the session token endpoints.
|
|
732
|
+
const FAKE_TOKEN = "aseph_ephemtesttoken12345678901234";
|
|
733
|
+
const FAKE_TOKEN_ID = "fake-ephem-token-id";
|
|
734
|
+
let mintCalled = false;
|
|
735
|
+
let revokeCalled = false;
|
|
736
|
+
let mintBody: Record<string, unknown> = {};
|
|
737
|
+
|
|
738
|
+
const swarmServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
739
|
+
if (req.method === "POST" && req.url === "/api/sessions/tokens") {
|
|
740
|
+
mintCalled = true;
|
|
741
|
+
const chunks: Buffer[] = [];
|
|
742
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
743
|
+
try {
|
|
744
|
+
mintBody = JSON.parse(Buffer.concat(chunks).toString()) as Record<string, unknown>;
|
|
745
|
+
} catch {
|
|
746
|
+
/* ignore */
|
|
747
|
+
}
|
|
748
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
749
|
+
res.end(JSON.stringify({ tokenId: FAKE_TOKEN_ID, plaintext: FAKE_TOKEN }));
|
|
750
|
+
} else if (req.method === "DELETE" && req.url === `/api/sessions/tokens/${FAKE_TOKEN_ID}`) {
|
|
751
|
+
revokeCalled = true;
|
|
752
|
+
res.writeHead(204);
|
|
753
|
+
res.end();
|
|
754
|
+
} else {
|
|
755
|
+
res.writeHead(404);
|
|
756
|
+
res.end();
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
await new Promise<void>((resolve) => swarmServer.listen(0, "127.0.0.1", resolve));
|
|
760
|
+
const swarmPort = (swarmServer.address() as import("net").AddressInfo).port;
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
const adapter = new ACPAdapter();
|
|
764
|
+
const session = await adapter.createSession(
|
|
765
|
+
baseConfig({
|
|
766
|
+
cwd,
|
|
767
|
+
apiUrl: `http://127.0.0.1:${swarmPort}`,
|
|
768
|
+
apiKey: "real-operator-key",
|
|
769
|
+
env: {
|
|
770
|
+
PATH: process.env.PATH ?? "",
|
|
771
|
+
HOME: process.env.HOME ?? "",
|
|
772
|
+
ACP_TARGET_COMMAND: "bun",
|
|
773
|
+
ACP_TARGET_ARGS: JSON.stringify([agentPath]),
|
|
774
|
+
},
|
|
775
|
+
}),
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
const events: ProviderEvent[] = [];
|
|
779
|
+
session.onEvent((e) => events.push(e));
|
|
780
|
+
const result = await session.waitForCompletion();
|
|
781
|
+
// Give the revoke call (fire-and-forget) a moment to complete.
|
|
782
|
+
await Bun.sleep(100);
|
|
783
|
+
|
|
784
|
+
expect(result.exitCode).toBe(0);
|
|
785
|
+
|
|
786
|
+
// The adapter must have minted an ephemeral token.
|
|
787
|
+
expect(mintCalled).toBe(true);
|
|
788
|
+
expect(mintBody).toMatchObject({
|
|
789
|
+
agentId: "agent-1",
|
|
790
|
+
taskId: "task-1",
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// The adapter must have revoked the token after the session ended.
|
|
794
|
+
expect(revokeCalled).toBe(true);
|
|
795
|
+
|
|
796
|
+
// Verify the bearer forwarded to the ACP target is the ephemeral token.
|
|
797
|
+
// The fake agent wrote the captured Authorization header value to a file
|
|
798
|
+
// during newSession; read it back here.
|
|
799
|
+
const captured = await Bun.file(captureFile).text();
|
|
800
|
+
expect(captured).toBe(`Bearer ${FAKE_TOKEN}`);
|
|
801
|
+
} finally {
|
|
802
|
+
await new Promise<void>((resolve) => swarmServer.close(() => resolve()));
|
|
803
|
+
}
|
|
804
|
+
});
|
|
553
805
|
});
|
|
@@ -157,6 +157,7 @@ describe("Phase 3 — /api/poll budget admission gate", () => {
|
|
|
157
157
|
if ("error" in body) throw new Error("unexpected error response");
|
|
158
158
|
expect(body.trigger?.type).toBe("task_assigned");
|
|
159
159
|
expect(body.trigger?.requestedBy).toEqual({
|
|
160
|
+
id: requester.id,
|
|
160
161
|
name: "Requester One",
|
|
161
162
|
email: "requester@example.com",
|
|
162
163
|
role: "engineering manager",
|
|
@@ -23,7 +23,7 @@ describe("build-pi-skills", () => {
|
|
|
23
23
|
.map((d) => d.name);
|
|
24
24
|
|
|
25
25
|
test("build script runs successfully", () => {
|
|
26
|
-
expect(buildOutput).toContain("Converted
|
|
26
|
+
expect(buildOutput).toContain("Converted 11 skills");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
test("every command has a corresponding pi-skill", () => {
|
|
@@ -75,12 +75,10 @@ describe("build-pi-skills", () => {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
describe("slash command syntax", () => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"review-offered-task",
|
|
83
|
-
];
|
|
78
|
+
// `work-on-task` / `review-offered-task` are seeded skills, not commands —
|
|
79
|
+
// they have no pi-skill directory. The commands that link to them still
|
|
80
|
+
// must render the reference as `/skill:work-on-task`.
|
|
81
|
+
const skillsWithCrossRefs = ["start-worker", "start-leader"];
|
|
84
82
|
|
|
85
83
|
for (const skill of skillsWithCrossRefs) {
|
|
86
84
|
test(`${skill} uses /skill: prefix for cross-references`, () => {
|