@desplega.ai/agent-swarm 1.162.0 → 1.164.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-74m73ae9.js → acp-adapter-8t22d18g.js} +2 -2
- package/dist/{actions-4sr14gnt.js → actions-49xrhww7.js} +32 -29
- package/dist/amp-adapter-p230tajt.js +34 -0
- package/dist/{anthropic-messages-8xxrzky9.js → anthropic-messages-hst62ws9.js} +3 -3
- package/dist/{app-pt68fbwp.js → app-tgzw3f3d.js} +11 -10
- package/dist/{assistant-cv3jytks.js → assistant-dgfjm07j.js} +33 -30
- package/dist/{azure-openai-responses-f5cqsyj8.js → azure-openai-responses-mh8tvf78.js} +9 -68
- package/dist/{boot-reembed-4ht4gqth.js → boot-reembed-0qzs614r.js} +13 -12
- package/dist/{boot-reembed-nxpcvpj9.js → boot-reembed-dgp6peyx.js} +12 -11
- package/dist/{boot-scrub-logs-k93kyw8k.js → boot-scrub-logs-fxnk021a.js} +10 -9
- package/dist/{boot-seeds-1skcrs0e.js → boot-seeds-xe7adjvm.js} +14 -13
- package/dist/{claude-adapter-r5g0tzge.js → claude-adapter-hx0n8a31.js} +22 -20
- package/dist/{claude-managed-adapter-yxkfxg8j.js → claude-managed-adapter-hskapeen.js} +3 -3
- package/dist/{claude-managed-setup-pkyrgshb.js → claude-managed-setup-p7mt6dfq.js} +1 -1
- package/dist/{claude-sdk-session-jxyxnrwd.js → claude-sdk-session-d61a9c8x.js} +22 -20
- package/dist/{cli-twrh04ap.js → cli-0y30kqz4.js} +3 -3
- package/dist/{cli-5xhpb3kv.js → cli-0z9dj97t.js} +9 -9
- package/dist/{cli-t845n9xk.js → cli-1nzb8ftr.js} +2 -2
- package/dist/{cli-kdkwd3yr.js → cli-1wd1n2ya.js} +38 -10
- package/dist/{cli-vxe6qa4h.js → cli-23az412e.js} +1 -1
- package/dist/{cli-bsky8g7w.js → cli-26x4d89j.js} +1 -1
- package/dist/{cli-swfmp1fx.js → cli-2ar0x7cz.js} +9 -9
- package/dist/cli-2ksgp01n.js +933 -0
- package/dist/{cli-rnyzn44p.js → cli-2zhhnz2h.js} +1 -1
- package/dist/{cli-vc9bp061.js → cli-3qaw32we.js} +5 -5
- package/dist/{cli-vy3ed88x.js → cli-402m8btc.js} +31 -3
- package/dist/{cli-5bcd7sfy.js → cli-43fh8fzw.js} +3 -3
- package/dist/{cli-5k6k8ta3.js → cli-4f7s6k3q.js} +13 -5
- package/dist/{cli-sqvfa090.js → cli-4ntg6tkp.js} +6 -5
- package/dist/{cli-kh04eqwe.js → cli-4y9bnpvy.js} +1 -1
- package/dist/{cli-qjjvzwzd.js → cli-5c2j5yvx.js} +1 -1
- package/dist/{cli-2zy95vhm.js → cli-6891ft0y.js} +2 -2
- package/dist/{cli-tre4bdgq.js → cli-6behs2bt.js} +1 -1
- package/dist/cli-7mm2r1kp.js +17 -0
- package/dist/cli-7xcgj20z.js +68 -0
- package/dist/{cli-4cwxn7gp.js → cli-812vt3ht.js} +2 -2
- package/dist/{cli-mbssg1xv.js → cli-8kt0bmgx.js} +1 -1
- package/dist/{cli-wjgs5wxe.js → cli-8rpq35c0.js} +7 -7
- package/dist/{cli-jxs2ygaw.js → cli-9m8fk2v7.js} +74 -29
- package/dist/{cli-pkd6s2wy.js → cli-9q3wt23f.js} +2 -2
- package/dist/{cli-s3hwpt9v.js → cli-9s6fsg07.js} +30 -10
- package/dist/{cli-xqpk3rp7.js → cli-9x3bwfd3.js} +28 -5
- package/dist/{cli-qrgt5bqf.js → cli-agamgm0p.js} +1 -1
- package/dist/{cli-13hw4z91.js → cli-b0f70z24.js} +1 -1
- package/dist/{cli-gpqpmns7.js → cli-cgrvsz2t.js} +2 -2
- package/dist/{cli-2hhfhfq5.js → cli-cn53e7vc.js} +8 -1
- package/dist/{cli-p0r8sxj5.js → cli-crjazzsg.js} +222 -4
- package/dist/{cli-tcen9p5w.js → cli-d37n8b7c.js} +5 -5
- package/dist/{cli-wxkkd0e0.js → cli-dgh781z9.js} +3 -3
- package/dist/cli-dsdg2wrk.js +516 -0
- package/dist/{cli-q24re23n.js → cli-e2xnc4m5.js} +4 -4
- package/dist/{cli-f7kvq46p.js → cli-erxeg64j.js} +20 -0
- package/dist/{cli-8ev9xqnx.js → cli-fajbc7sa.js} +17 -219
- package/dist/{cli-g739zqzt.js → cli-fj86xr3c.js} +2 -2
- package/dist/{cli-zvhd3f64.js → cli-fpbvcf55.js} +3 -3
- package/dist/{cli-kp42yek0.js → cli-fyra5y8k.js} +424 -71
- package/dist/{cli-jyy6pm2f.js → cli-g3qgrzck.js} +15 -3
- package/dist/{cli-eg0b3p0k.js → cli-g5erf0zt.js} +23 -14
- package/dist/{cli-cvatsam7.js → cli-gcvzvq3e.js} +4 -4
- package/dist/{cli-89s4ngw3.js → cli-gvcgmx69.js} +1 -1
- package/dist/{cli-7tre70zp.js → cli-j261c831.js} +138 -128
- package/dist/{cli-sptrj5f5.js → cli-j3me0jw2.js} +2 -2
- package/dist/{cli-zxqx25nz.js → cli-k6w44yvq.js} +6 -6
- package/dist/cli-ndx5yz5n.js +3299 -0
- package/dist/{cli-ykk5jaax.js → cli-nexwh8zg.js} +34 -29
- package/dist/{cli-rfjw9jw2.js → cli-ng97fjq7.js} +2 -2
- package/dist/{cli-4x0csdjq.js → cli-ngne1dng.js} +2 -2
- package/dist/{cli-4dcpaefp.js → cli-pfzrhb8n.js} +2 -2
- package/dist/{cli-hb5c2x5y.js → cli-q38n4bq0.js} +2 -2
- package/dist/{cli-cebmgwsc.js → cli-r109nq7n.js} +4 -4
- package/dist/{cli-nq7yqw7b.js → cli-t7g0vmtc.js} +4 -4
- package/dist/{cli-jf7wmv1r.js → cli-tsvj33y0.js} +58 -19
- package/dist/{cli-7af8yhav.js → cli-vbr859rz.js} +7 -4
- package/dist/{cli-nt8hecg9.js → cli-wk3v6h55.js} +67 -38
- package/dist/{cli-vyqs9ya8.js → cli-wwvk0tsy.js} +7 -7
- package/dist/{cli-rj0gqwwp.js → cli-y2de3bzp.js} +1 -1
- package/dist/{cli-hmsw1gpy.js → cli-y74170pb.js} +4 -4
- package/dist/cli-ydt3zkwv.js +70 -0
- package/dist/{cli-xk79303e.js → cli-yj790e19.js} +9 -1
- package/dist/cli-zt3w1cn1.js +137 -0
- package/dist/{cli-trc24wk2.js → cli-zxg80zs9.js} +1 -1
- package/dist/cli.js +23 -22
- package/dist/{cloudflare-workers-ai-system-one-k8ddqjkq.js → cloudflare-workers-ai-system-one-tt484t61.js} +2 -2
- package/dist/{codex-adapter-esvmnhn7.js → codex-adapter-e3pky8aj.js} +14 -13
- package/dist/{codex-hook-m73jmqgx.js → codex-hook-b1fb7c2k.js} +4 -3
- package/dist/{codex-session-runner-ban9excr.js → codex-session-runner-1gq27w2t.js} +14 -13
- package/dist/{commands-75413p2n.js → commands-q07ezjzn.js} +10 -9
- package/dist/cursor-adapter-50cz7yba.js +32 -0
- package/dist/{db-7f5pecwg.js → db-wwt0sq9q.js} +18 -9
- package/dist/{dispatcher-fh9at8nz.js → dispatcher-wrg35vdd.js} +31 -28
- package/dist/{dsh-adapter-wpze65d4.js → dsh-adapter-sa6rxfpc.js} +6 -6
- package/dist/{e2b-gjgdjp91.js → e2b-v5np73yj.js} +4 -4
- package/dist/{easter-egg-3d-3w5c2n4c.js → easter-egg-3d-h2hx9b64.js} +2 -2
- package/dist/{event-bus-sfx8dx3t.js → event-bus-vkbhzt4n.js} +1 -1
- package/dist/{execute-f50wg9nh.js → execute-6gczxy8v.js} +42 -13
- package/dist/{google-generative-ai-jxxpnegy.js → google-generative-ai-d6nphg3q.js} +3 -3
- package/dist/{google-vertex-x7q1angm.js → google-vertex-cpshkref.js} +3 -3
- package/dist/{handlers-w13787bb.js → handlers-rv3gxvsp.js} +34 -31
- package/dist/{hook-46g1jrvx.js → hook-bcf5m80y.js} +15 -13
- package/dist/{hook-4tpts2ry.js → hook-z8b1jm2d.js} +16 -14
- package/dist/{http-19pzbfrt.js → http-56f7yt97.js} +932 -134
- package/dist/{index-scptvdxg.js → index-0kncjnrm.js} +30 -23
- package/dist/{index-dqmp15bq.js → index-1e6cg96n.js} +31 -28
- package/dist/{index-qb4q1y3e.js → index-8xgdb3k8.js} +31 -28
- package/dist/index-adw4zr9j.js +1020 -0
- package/dist/{index-h5gs261z.js → index-etdrp7yp.js} +140 -140
- package/dist/{index-8fd4dm4h.js → index-f9zscmft.js} +32 -29
- package/dist/index-fdk1t4d6.js +91297 -0
- package/dist/{index-s75wc4wd.js → index-jvb37h77.js} +12 -11
- package/dist/{keepalive-xa5ts9cd.js → keepalive-rp8daj4s.js} +12 -11
- package/dist/{lead-qp4xkvjq.js → lead-dfp77erc.js} +45 -40
- package/dist/{maintenance-rm5885hm.js → maintenance-wtmw3jqv.js} +13 -12
- package/dist/{mistral-conversations-rw7fbt22.js → mistral-conversations-8445ddjn.js} +2 -2
- package/dist/{oauth-refresh-sweep-1csdfebf.js → oauth-refresh-sweep-yhzqt95v.js} +11 -10
- package/dist/{onboard-r6b78cs4.js → onboard-9z8rwbcy.js} +2 -2
- package/dist/{openai-codex-responses-efvrgrq3.js → openai-codex-responses-9ynm88zy.js} +3 -3
- package/dist/{openai-completions-bbbrrz4z.js → openai-completions-nhqwjmaw.js} +2 -2
- package/dist/{openai-responses-9jjyrcm1.js → openai-responses-xx1c4kgm.js} +3 -3
- package/dist/{opencode-adapter-kb3g6pkf.js → opencode-adapter-1nxb6555.js} +5 -5
- package/dist/{otel-impl-0y9knpky.js → otel-impl-b83gaxw2.js} +1 -1
- package/dist/{pi-mono-adapter-z6s1bme2.js → pi-mono-adapter-ddxz8pjp.js} +18 -17
- package/dist/{pricing-refresh-mth26tgp.js → pricing-refresh-tmjvazb6.js} +12 -11
- package/dist/{rbac-roles-kwntb23q.js → rbac-roles-pa0gzjws.js} +11 -10
- package/dist/{rbac-roles-vpk4ebm8.js → rbac-roles-rhexa9s2.js} +12 -11
- package/dist/{render-v2-hcm7h00y.js → render-v2-ask84n8y.js} +11 -10
- package/dist/{runtime-vfjsfsc5.js → runtime-ckjnj3vf.js} +4 -4
- package/dist/{setup-hbyzd65h.js → setup-5ga5gdsy.js} +2 -2
- package/dist/{typesafe-system-one-2ef30pec.js → typesafe-system-one-datfr82p.js} +2 -2
- package/dist/{virtual-modules-9bv7ry91.js → virtual-modules-bmp3zwjf.js} +12 -11
- package/dist/{work-objects-m09sxnhe.js → work-objects-snnh0t7j.js} +34 -31
- package/dist/{worker-0h35rvk4.js → worker-rxyymrhb.js} +45 -40
- package/openapi.json +997 -39
- package/package.json +6 -5
- package/src/be/db/active-sessions.ts +30 -0
- package/src/be/db/approvals.ts +54 -0
- package/src/be/db.ts +25 -4
- package/src/be/mcp-server-checked-update.ts +34 -0
- package/src/be/mcp-server-stdio-gate.ts +34 -0
- package/src/be/memory/key-browser.ts +304 -0
- package/src/be/memory/providers/sqlite-store.ts +16 -3
- package/src/be/migrations/194_memory_orphan_chunks.sql +15 -0
- package/src/be/migrations/195_approval_request_approvals.sql +6 -0
- package/src/be/model-validation.ts +12 -2
- package/src/be/multi-runtime.ts +4 -3
- package/src/be/pricing-normalize.ts +11 -0
- package/src/be/repo-checked-update.ts +37 -0
- package/src/be/repo-merge-policy.ts +15 -0
- package/src/be/seed-pricing.ts +48 -1
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/provider-credentials.ts +44 -2
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +100 -0
- package/src/extensions/dispatcher.ts +19 -0
- package/src/extensions/post-bridge.ts +191 -0
- package/src/github/handlers.ts +128 -2
- package/src/gitlab/workflow-action-name.ts +15 -0
- package/src/heartbeat/heartbeat.ts +4 -3
- package/src/http/active-sessions.ts +6 -5
- package/src/http/agents.ts +10 -1
- package/src/http/approval-requests.ts +271 -35
- package/src/http/index.ts +1 -1
- package/src/http/mcp-servers.ts +74 -50
- package/src/http/memory.ts +275 -6
- package/src/http/repos.ts +58 -18
- package/src/http/request-principal.ts +19 -0
- package/src/http/session-cost-recompute.ts +64 -0
- package/src/http/session-data.ts +11 -0
- package/src/http/tasks.ts +9 -1
- package/src/http/webhooks.ts +17 -5
- package/src/prompts/base-prompt.ts +9 -4
- package/src/prompts/resolver.ts +1 -1
- package/src/prompts/session-templates.ts +28 -0
- package/src/providers/amp-adapter.ts +1232 -0
- package/src/providers/cursor-adapter.ts +638 -0
- package/src/providers/index.ts +9 -1
- package/src/providers/pricing-sources.md +39 -0
- package/src/providers/reasoning-effort.ts +15 -1
- package/src/providers/types.ts +11 -1
- package/src/rbac/legacy-policy.ts +23 -8
- package/src/rbac/permissions.ts +27 -1
- package/src/scripts-runtime/types/swarm-extension.d.ts +119 -0
- package/src/tests/amp-adapter.test.ts +822 -0
- package/src/tests/amp-budget-export-failed.test.ts +133 -0
- package/src/tests/amp-pricing-helpers.ts +16 -0
- package/src/tests/amp-pricing.test.ts +218 -0
- package/src/tests/api-drain-worker-e2e.test.ts +18 -7
- package/src/tests/approval-respond-auth.test.ts +371 -0
- package/src/tests/base-prompt.test.ts +13 -0
- package/src/tests/claude-adapter-binary.test.ts +24 -0
- package/src/tests/claude-adapter.test.ts +8 -0
- package/src/tests/cursor-adapter.test.ts +690 -0
- package/src/tests/db-task-read-contract.test.ts +5 -0
- package/src/tests/db-task-write-contract.test.ts +5 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-post-bridge.test.ts +669 -0
- package/src/tests/extensions-run-context.test.ts +61 -0
- package/src/tests/fixtures/fake-amp.ts +304 -0
- package/src/tests/github-handlers-inline-comments.test.ts +147 -1
- package/src/tests/gitlab-workflow-event-names.test.ts +136 -0
- package/src/tests/mcp-input-ergonomics.test.ts +15 -10
- package/src/tests/mcp-server-stdio-gate.test.ts +438 -0
- package/src/tests/memory-http-access.test.ts +221 -0
- package/src/tests/memory-keys-endpoint.test.ts +406 -0
- package/src/tests/memory-store.test.ts +166 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -4
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-process-group.test.ts +41 -0
- package/src/tests/provider-registration.test.ts +3 -1
- package/src/tests/rbac-charact-misc-tools.test.ts +18 -3
- package/src/tests/rbac-engine.test.ts +15 -11
- package/src/tests/rbac-wire-e2e.test.ts +4 -9
- package/src/tests/repo-allow-merge-gate.test.ts +382 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/send-task-requester-gate.test.ts +260 -0
- package/src/tests/session-activity-liveness.test.ts +248 -0
- package/src/tests/session-costs-golden.test.ts +28 -0
- package/src/tests/webhooks-task-blocked.test.ts +4 -4
- package/src/tests/write-gate.ts +68 -0
- package/src/tests/x402-client.test.ts +124 -0
- package/src/tests/x402-spending-tracker.test.ts +66 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +23 -1
- package/src/tools/mcp-servers/mcp-server-update.ts +46 -28
- package/src/tools/memory-delete.ts +3 -3
- package/src/tools/repos/update-repo.ts +26 -5
- package/src/tools/send-task.ts +24 -1
- package/src/tools/steer-task.ts +1 -1
- package/src/types.ts +37 -2
- package/src/utils/amp-models.ts +114 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/process-group.ts +79 -0
- package/src/workflows/event-bus.ts +28 -0
- package/src/x402/client.ts +36 -14
- package/src/x402/spending-tracker.ts +64 -3
- package/templates/skills/agent-browser/SKILL.md +5 -0
- package/templates/skills/agent-browser/content.md +5 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -0
- package/templates/skills/swarm-extensions/content.md +4 -0
- package/dist/cli-y89t4nca.js +0 -67
package/src/http/memory.ts
CHANGED
|
@@ -7,6 +7,16 @@ import { CANDIDATE_SET_MULTIPLIER } from "../be/memory/constants";
|
|
|
7
7
|
import { listEdgesForAgent } from "../be/memory/edges-store";
|
|
8
8
|
import { expandCandidatesWithGraph } from "../be/memory/graph-expansion";
|
|
9
9
|
import { indexMemoryContent } from "../be/memory/index-content";
|
|
10
|
+
import {
|
|
11
|
+
checkChunkIntegrity,
|
|
12
|
+
estimateTokens,
|
|
13
|
+
getMemoryChunks,
|
|
14
|
+
getPosteriorsByIds,
|
|
15
|
+
listMemoryKeys,
|
|
16
|
+
MEMORY_KEYS_DEFAULT_PREFIX,
|
|
17
|
+
MEMORY_KEYS_MAX_LIMIT,
|
|
18
|
+
posteriorMean,
|
|
19
|
+
} from "../be/memory/key-browser";
|
|
10
20
|
import { MemoryKeyError } from "../be/memory/key-guard";
|
|
11
21
|
import { MEMORY_KEY_MAX_LENGTH } from "../be/memory/key-paths";
|
|
12
22
|
import { refreshLinks } from "../be/memory/link-resolver";
|
|
@@ -223,15 +233,24 @@ const MemoryListResultItemSchema = z.object({
|
|
|
223
233
|
chunkIndex: z.number(),
|
|
224
234
|
totalChunks: z.number(),
|
|
225
235
|
tags: z.array(z.string()),
|
|
236
|
+
key: z.string().nullable(),
|
|
237
|
+
updatedAt: z.string().nullable(),
|
|
238
|
+
rating: z.number().describe("Usefulness posterior mean alpha / (alpha + beta); 0.5 = no signal"),
|
|
226
239
|
});
|
|
227
240
|
|
|
228
241
|
const listMemory = route({
|
|
229
242
|
method: "post",
|
|
230
243
|
path: "/api/memory/list",
|
|
231
244
|
pattern: ["api", "memory", "list"],
|
|
232
|
-
summary: "List or semantically search memories
|
|
245
|
+
summary: "List or semantically search memories (debug/admin)",
|
|
246
|
+
description:
|
|
247
|
+
"The operator key, a user, and the lead see every agent's memories. Any other agent (an `aseph_` session token, or the shared key with `X-Agent-ID`) sees only its own memories and swarm-scope memories.",
|
|
233
248
|
tags: ["Memory"],
|
|
234
249
|
auth: { apiKey: true },
|
|
250
|
+
rbac: {
|
|
251
|
+
ungated:
|
|
252
|
+
"read-only listing; the handler narrows an agent principal to its own and swarm-scope rows",
|
|
253
|
+
},
|
|
235
254
|
body: z.object({
|
|
236
255
|
query: z
|
|
237
256
|
.string()
|
|
@@ -266,6 +285,136 @@ const listMemory = route({
|
|
|
266
285
|
},
|
|
267
286
|
});
|
|
268
287
|
|
|
288
|
+
// Mirrors `MemoryKeySummary` (src/be/memory/key-browser.ts).
|
|
289
|
+
const MemoryKeySummarySchema = z.object({
|
|
290
|
+
key: z.string(),
|
|
291
|
+
scope: AgentMemoryScopeSchema,
|
|
292
|
+
agentId: z.string().nullable(),
|
|
293
|
+
memoryId: z.string(),
|
|
294
|
+
name: z.string(),
|
|
295
|
+
source: AgentMemorySourceSchema,
|
|
296
|
+
chunkRows: z.number(),
|
|
297
|
+
totalChunks: z.number(),
|
|
298
|
+
complete: z.boolean(),
|
|
299
|
+
chars: z.number(),
|
|
300
|
+
estTokens: z.number(),
|
|
301
|
+
accessCount: z.number(),
|
|
302
|
+
lastAccessedAt: z.string().nullable(),
|
|
303
|
+
rating: z.number(),
|
|
304
|
+
alpha: z.number(),
|
|
305
|
+
beta: z.number(),
|
|
306
|
+
usefulRatings: z.number(),
|
|
307
|
+
notUsefulRatings: z.number(),
|
|
308
|
+
createdAt: z.string(),
|
|
309
|
+
updatedAt: z.string(),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const listMemoryKeysRoute = route({
|
|
313
|
+
method: "get",
|
|
314
|
+
path: "/api/memory/keys",
|
|
315
|
+
pattern: ["api", "memory", "keys"],
|
|
316
|
+
summary:
|
|
317
|
+
"One aggregate row per keyed memory under a key prefix: chunks, usage, rating, estimated tokens (debug/admin)",
|
|
318
|
+
description:
|
|
319
|
+
"The operator key, a user, and the lead see every agent's memories. Any other agent sees only its own memories and swarm-scope memories.",
|
|
320
|
+
tags: ["Memory"],
|
|
321
|
+
auth: { apiKey: true },
|
|
322
|
+
query: z.object({
|
|
323
|
+
prefix: z
|
|
324
|
+
.string()
|
|
325
|
+
.min(1)
|
|
326
|
+
.max(MEMORY_KEY_MAX_LENGTH)
|
|
327
|
+
.default(MEMORY_KEYS_DEFAULT_PREFIX)
|
|
328
|
+
.describe("Literal, case-sensitive key prefix (default '/longterm/')."),
|
|
329
|
+
limit: z.coerce.number().int().min(1).max(MEMORY_KEYS_MAX_LIMIT).default(MEMORY_KEYS_MAX_LIMIT),
|
|
330
|
+
}),
|
|
331
|
+
responses: {
|
|
332
|
+
200: {
|
|
333
|
+
description:
|
|
334
|
+
"Keyed memories grouped by (key, scope, agentId). accessCount is summed over chunk rows; rating pools alpha/beta over chunk rows; estTokens = ceil(chars / 4)",
|
|
335
|
+
schema: z.object({
|
|
336
|
+
prefix: z.string(),
|
|
337
|
+
keys: z.array(MemoryKeySummarySchema),
|
|
338
|
+
truncated: z.boolean(),
|
|
339
|
+
}),
|
|
340
|
+
},
|
|
341
|
+
400: { description: "Validation error" },
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const MemoryChunkSchema = z.object({
|
|
346
|
+
id: z.string(),
|
|
347
|
+
agentId: z.string().nullable(),
|
|
348
|
+
scope: AgentMemoryScopeSchema,
|
|
349
|
+
key: z.string().nullable(),
|
|
350
|
+
name: z.string(),
|
|
351
|
+
content: z.string(),
|
|
352
|
+
source: AgentMemorySourceSchema,
|
|
353
|
+
sourceTaskId: z.string().nullable(),
|
|
354
|
+
sourcePath: z.string().nullable(),
|
|
355
|
+
chunkIndex: z.number(),
|
|
356
|
+
totalChunks: z.number(),
|
|
357
|
+
tags: z.array(z.string()),
|
|
358
|
+
createdAt: z.string(),
|
|
359
|
+
updatedAt: z.string().nullable(),
|
|
360
|
+
accessedAt: z.string(),
|
|
361
|
+
expiresAt: z.string().nullable(),
|
|
362
|
+
accessCount: z.number(),
|
|
363
|
+
embeddingModel: z.string().nullable(),
|
|
364
|
+
rating: z.number(),
|
|
365
|
+
alpha: z.number(),
|
|
366
|
+
beta: z.number(),
|
|
367
|
+
version: z.number(),
|
|
368
|
+
estTokens: z.number(),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const getMemoryChunksRoute = route({
|
|
372
|
+
method: "get",
|
|
373
|
+
path: "/api/memory/chunks",
|
|
374
|
+
pattern: ["api", "memory", "chunks"],
|
|
375
|
+
summary:
|
|
376
|
+
"Every chunk row of one memory in chunkIndex order, with a chunk-integrity check (debug/admin). Does not count as an access",
|
|
377
|
+
description:
|
|
378
|
+
"Visibility matches the memory list: an agent other than the lead gets 404 for another agent's agent-scope memory.",
|
|
379
|
+
tags: ["Memory"],
|
|
380
|
+
auth: { apiKey: true },
|
|
381
|
+
query: z
|
|
382
|
+
.object({
|
|
383
|
+
memoryId: z.string().uuid().optional().describe("Any chunk row of the memory."),
|
|
384
|
+
key: z.string().min(1).max(MEMORY_KEY_MAX_LENGTH).optional(),
|
|
385
|
+
scope: AgentMemoryScopeSchema.optional(),
|
|
386
|
+
agentId: z
|
|
387
|
+
.string()
|
|
388
|
+
.optional()
|
|
389
|
+
.describe("Owner agent id; pass an empty string for rows without one."),
|
|
390
|
+
})
|
|
391
|
+
.refine((q) => q.memoryId || q.key, { message: "memoryId or key required" }),
|
|
392
|
+
responses: {
|
|
393
|
+
200: {
|
|
394
|
+
description: "Chunk rows and integrity findings",
|
|
395
|
+
schema: z.object({
|
|
396
|
+
key: z.string().nullable(),
|
|
397
|
+
scope: AgentMemoryScopeSchema,
|
|
398
|
+
agentId: z.string().nullable(),
|
|
399
|
+
chunks: z.array(MemoryChunkSchema),
|
|
400
|
+
estTokens: z.number(),
|
|
401
|
+
integrity: z.object({
|
|
402
|
+
ok: z.boolean(),
|
|
403
|
+
expectedChunks: z.number(),
|
|
404
|
+
presentIndexes: z.array(z.number()),
|
|
405
|
+
missingIndexes: z.array(z.number()),
|
|
406
|
+
duplicateIndexes: z.array(z.number()),
|
|
407
|
+
conflictingTotals: z.array(z.number()),
|
|
408
|
+
outOfRangeIds: z.array(z.string()),
|
|
409
|
+
issues: z.array(z.string()),
|
|
410
|
+
}),
|
|
411
|
+
}),
|
|
412
|
+
},
|
|
413
|
+
400: { description: "Validation error" },
|
|
414
|
+
404: { description: "Memory not found" },
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
269
418
|
// Mirrors `MemoryVecPopulateStats` (src/be/memory/types.ts).
|
|
270
419
|
const MemoryVecPopulateStatsSchema = z.object({
|
|
271
420
|
attempted: z.number(),
|
|
@@ -395,11 +544,15 @@ const deleteMemoryById = route({
|
|
|
395
544
|
path: "/api/memory/{id}",
|
|
396
545
|
pattern: ["api", "memory", null],
|
|
397
546
|
summary: "Delete a single memory by ID (debug/admin)",
|
|
547
|
+
description:
|
|
548
|
+
"The operator key and users may delete any memory. The lead may delete its own memories and swarm-scope memories. Any other agent may delete only its own agent-scope memories.",
|
|
398
549
|
tags: ["Memory"],
|
|
399
550
|
auth: { apiKey: true },
|
|
551
|
+
rbac: { permission: "memory.delete.any" },
|
|
400
552
|
params: z.object({ id: z.string().uuid() }),
|
|
401
553
|
responses: {
|
|
402
554
|
200: { description: "Memory deleted", schema: z.object({ deleted: z.boolean() }) },
|
|
555
|
+
403: { description: "Caller may not delete this memory" },
|
|
403
556
|
404: { description: "Memory not found" },
|
|
404
557
|
},
|
|
405
558
|
});
|
|
@@ -643,6 +796,12 @@ async function ingestPrincipal(
|
|
|
643
796
|
return { kind: "agent", agentId: "", isLead: false };
|
|
644
797
|
}
|
|
645
798
|
|
|
799
|
+
/** Whose rows a memory list may show. `seesAll` lifts the own-or-swarm filter. */
|
|
800
|
+
function memoryListViewer(principal: RbacPrincipal): { agentId: string; seesAll: boolean } {
|
|
801
|
+
const seesAll = can({ principal, verb: "memory.read.any", source: "http" }).allow;
|
|
802
|
+
return { agentId: principal.kind === "agent" ? principal.agentId : "", seesAll };
|
|
803
|
+
}
|
|
804
|
+
|
|
646
805
|
export async function handleMemory(
|
|
647
806
|
req: IncomingMessage,
|
|
648
807
|
res: ServerResponse,
|
|
@@ -852,6 +1011,9 @@ export async function handleMemory(
|
|
|
852
1011
|
|
|
853
1012
|
const { query, agentId, scope, source, sourcePath, limit, offset } = parsed.body;
|
|
854
1013
|
const store = getMemoryStore();
|
|
1014
|
+
// Humans and the lead see every agent's rows; any other agent sees its own
|
|
1015
|
+
// rows plus swarm-scope rows (the store's non-lead scope conditions).
|
|
1016
|
+
const viewer = memoryListViewer(await ingestPrincipal(req, myAgentId));
|
|
855
1017
|
const pageLimit = Math.min(limit, 100);
|
|
856
1018
|
const pathNeedle = sourcePath?.trim().toLowerCase();
|
|
857
1019
|
const matchesPath = (p: string | null) =>
|
|
@@ -866,10 +1028,10 @@ export async function handleMemory(
|
|
|
866
1028
|
4096,
|
|
867
1029
|
Math.max(offset + pageLimit, pageLimit) * CANDIDATE_SET_MULTIPLIER,
|
|
868
1030
|
);
|
|
869
|
-
let candidates = await store.search(queryEmbedding ?? new Float32Array(0), agentId
|
|
1031
|
+
let candidates = await store.search(queryEmbedding ?? new Float32Array(0), viewer.agentId, {
|
|
870
1032
|
scope,
|
|
871
1033
|
limit: candidateLimit,
|
|
872
|
-
isLead:
|
|
1034
|
+
isLead: viewer.seesAll,
|
|
873
1035
|
source,
|
|
874
1036
|
queryText: query.trim(),
|
|
875
1037
|
});
|
|
@@ -904,6 +1066,9 @@ export async function handleMemory(
|
|
|
904
1066
|
chunkIndex: r.chunkIndex,
|
|
905
1067
|
totalChunks: r.totalChunks,
|
|
906
1068
|
tags: r.tags,
|
|
1069
|
+
key: r.key ?? null,
|
|
1070
|
+
updatedAt: r.updatedAt ?? null,
|
|
1071
|
+
rating: posteriorMean(r.alpha, r.beta),
|
|
907
1072
|
})),
|
|
908
1073
|
total: candidates.length,
|
|
909
1074
|
limit: pageLimit,
|
|
@@ -917,13 +1082,14 @@ export async function handleMemory(
|
|
|
917
1082
|
scope,
|
|
918
1083
|
limit: pageLimit,
|
|
919
1084
|
offset,
|
|
920
|
-
isLead:
|
|
1085
|
+
isLead: viewer.seesAll,
|
|
921
1086
|
ownerAgentId: agentId,
|
|
922
1087
|
source,
|
|
923
1088
|
sourcePath: pathNeedle,
|
|
924
1089
|
};
|
|
925
|
-
const rows = await store.list(agentId
|
|
926
|
-
const total = await store.count(agentId
|
|
1090
|
+
const rows = await store.list(viewer.agentId, listOptions);
|
|
1091
|
+
const total = await store.count(viewer.agentId, listOptions);
|
|
1092
|
+
const posteriors = await getPosteriorsByIds(rows.map((r) => r.id));
|
|
927
1093
|
|
|
928
1094
|
listMemory.respond(res, 200, {
|
|
929
1095
|
results: rows.map((r) => ({
|
|
@@ -943,6 +1109,12 @@ export async function handleMemory(
|
|
|
943
1109
|
chunkIndex: r.chunkIndex,
|
|
944
1110
|
totalChunks: r.totalChunks,
|
|
945
1111
|
tags: r.tags,
|
|
1112
|
+
key: r.key ?? null,
|
|
1113
|
+
updatedAt: r.updatedAt ?? null,
|
|
1114
|
+
rating: (() => {
|
|
1115
|
+
const p = posteriors.get(r.id);
|
|
1116
|
+
return p ? posteriorMean(p.alpha, p.beta) : 0.5;
|
|
1117
|
+
})(),
|
|
946
1118
|
})),
|
|
947
1119
|
total,
|
|
948
1120
|
limit: pageLimit,
|
|
@@ -956,6 +1128,88 @@ export async function handleMemory(
|
|
|
956
1128
|
return true;
|
|
957
1129
|
}
|
|
958
1130
|
|
|
1131
|
+
if (listMemoryKeysRoute.match(req.method, pathSegments)) {
|
|
1132
|
+
const parsed = await listMemoryKeysRoute.parse(
|
|
1133
|
+
req,
|
|
1134
|
+
res,
|
|
1135
|
+
pathSegments,
|
|
1136
|
+
parseQueryParams(req.url || ""),
|
|
1137
|
+
);
|
|
1138
|
+
if (!parsed) return true;
|
|
1139
|
+
const { prefix, limit } = parsed.query;
|
|
1140
|
+
const viewer = memoryListViewer(await ingestPrincipal(req, myAgentId));
|
|
1141
|
+
const { keys, truncated } = await listMemoryKeys({
|
|
1142
|
+
prefix,
|
|
1143
|
+
limit,
|
|
1144
|
+
visibleToAgentId: viewer.seesAll ? undefined : viewer.agentId,
|
|
1145
|
+
});
|
|
1146
|
+
listMemoryKeysRoute.respond(res, 200, {
|
|
1147
|
+
prefix,
|
|
1148
|
+
keys: keys.map((k) => ({
|
|
1149
|
+
...k,
|
|
1150
|
+
scope: AgentMemoryScopeSchema.parse(k.scope),
|
|
1151
|
+
source: AgentMemorySourceSchema.parse(k.source),
|
|
1152
|
+
})),
|
|
1153
|
+
truncated,
|
|
1154
|
+
});
|
|
1155
|
+
return true;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (getMemoryChunksRoute.match(req.method, pathSegments)) {
|
|
1159
|
+
const parsed = await getMemoryChunksRoute.parse(
|
|
1160
|
+
req,
|
|
1161
|
+
res,
|
|
1162
|
+
pathSegments,
|
|
1163
|
+
parseQueryParams(req.url || ""),
|
|
1164
|
+
);
|
|
1165
|
+
if (!parsed) return true;
|
|
1166
|
+
const { memoryId, key, scope, agentId } = parsed.query;
|
|
1167
|
+
// Same visibility as the list: a hidden memory reads as not found.
|
|
1168
|
+
const viewer = memoryListViewer(await ingestPrincipal(req, myAgentId));
|
|
1169
|
+
const visibility = { visibleToAgentId: viewer.seesAll ? undefined : viewer.agentId };
|
|
1170
|
+
const doc = memoryId
|
|
1171
|
+
? await getMemoryChunks({ memoryId }, visibility)
|
|
1172
|
+
: await getMemoryChunks({ key: key as string, scope, agentId }, visibility);
|
|
1173
|
+
if (!doc) {
|
|
1174
|
+
jsonError(res, "Memory not found", 404);
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
const chunks = doc.chunks.map((c) => ({
|
|
1178
|
+
id: c.id,
|
|
1179
|
+
agentId: c.agentId,
|
|
1180
|
+
scope: AgentMemoryScopeSchema.parse(c.scope),
|
|
1181
|
+
key: c.key,
|
|
1182
|
+
name: c.name,
|
|
1183
|
+
content: c.content,
|
|
1184
|
+
source: AgentMemorySourceSchema.parse(c.source),
|
|
1185
|
+
sourceTaskId: c.sourceTaskId,
|
|
1186
|
+
sourcePath: c.sourcePath,
|
|
1187
|
+
chunkIndex: c.chunkIndex,
|
|
1188
|
+
totalChunks: c.totalChunks,
|
|
1189
|
+
tags: c.tags,
|
|
1190
|
+
createdAt: c.createdAt,
|
|
1191
|
+
updatedAt: c.updatedAt ?? null,
|
|
1192
|
+
accessedAt: c.accessedAt,
|
|
1193
|
+
expiresAt: c.expiresAt ?? null,
|
|
1194
|
+
accessCount: c.accessCount ?? 0,
|
|
1195
|
+
embeddingModel: c.embeddingModel ?? null,
|
|
1196
|
+
rating: posteriorMean(c.alpha, c.beta),
|
|
1197
|
+
alpha: c.alpha,
|
|
1198
|
+
beta: c.beta,
|
|
1199
|
+
version: c.version ?? 1,
|
|
1200
|
+
estTokens: estimateTokens(c.content.length),
|
|
1201
|
+
}));
|
|
1202
|
+
getMemoryChunksRoute.respond(res, 200, {
|
|
1203
|
+
key: doc.key,
|
|
1204
|
+
scope: AgentMemoryScopeSchema.parse(doc.scope),
|
|
1205
|
+
agentId: doc.agentId,
|
|
1206
|
+
chunks,
|
|
1207
|
+
estTokens: estimateTokens(chunks.reduce((sum, c) => sum + c.content.length, 0)),
|
|
1208
|
+
integrity: checkChunkIntegrity(chunks),
|
|
1209
|
+
});
|
|
1210
|
+
return true;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
959
1213
|
if (editMemory.match(req.method, pathSegments)) {
|
|
960
1214
|
if (!myAgentId) {
|
|
961
1215
|
jsonError(res, "Missing X-Agent-ID header", 400);
|
|
@@ -1062,6 +1316,21 @@ export async function handleMemory(
|
|
|
1062
1316
|
if (!parsed) return true;
|
|
1063
1317
|
|
|
1064
1318
|
const store = getMemoryStore();
|
|
1319
|
+
const memory = await store.peek(parsed.params.id);
|
|
1320
|
+
if (!memory) {
|
|
1321
|
+
jsonError(res, "Memory not found", 404);
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
const decision = can({
|
|
1325
|
+
principal: await ingestPrincipal(req, myAgentId),
|
|
1326
|
+
verb: "memory.delete.any",
|
|
1327
|
+
resource: { kind: "owned", ownerAgentId: memory.agentId, scope: memory.scope },
|
|
1328
|
+
source: "http",
|
|
1329
|
+
});
|
|
1330
|
+
if (!decision.allow) {
|
|
1331
|
+
jsonError(res, `Forbidden: ${decision.reason}`, 403);
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1065
1334
|
const deleted = await store.delete(parsed.params.id);
|
|
1066
1335
|
if (!deleted) {
|
|
1067
1336
|
jsonError(res, "Memory not found", 404);
|
package/src/http/repos.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
getSwarmRepos,
|
|
8
|
-
updateSwarmRepo,
|
|
9
|
-
} from "../be/db";
|
|
3
|
+
import { createSwarmRepo, deleteSwarmRepo, getSwarmRepoById, getSwarmRepos } from "../be/db";
|
|
4
|
+
import { updateSwarmRepoChecked } from "../be/repo-checked-update";
|
|
5
|
+
import { changesAllowMerge } from "../be/repo-merge-policy";
|
|
6
|
+
import { can } from "../rbac";
|
|
10
7
|
import { emitIntegrationConnected } from "../telemetry";
|
|
11
8
|
import { RepoGuidelinesInputSchema, RepoHooksSchema, SwarmRepoSchema } from "../types";
|
|
9
|
+
import { agentFirstPrincipal } from "./request-principal";
|
|
12
10
|
import { route } from "./route-def";
|
|
13
11
|
import { json, jsonError } from "./utils";
|
|
14
12
|
|
|
@@ -63,8 +61,13 @@ const createRepo = route({
|
|
|
63
61
|
responses: {
|
|
64
62
|
201: { description: "Repo created", schema: SwarmRepoSchema },
|
|
65
63
|
400: { description: "Validation error", schema: z.object({ error: z.string() }) },
|
|
64
|
+
403: {
|
|
65
|
+
description: "Only the lead, the operator or a user can turn allowMerge on",
|
|
66
|
+
schema: z.object({ error: z.string() }),
|
|
67
|
+
},
|
|
66
68
|
409: { description: "Duplicate repo", schema: z.object({ error: z.string() }) },
|
|
67
69
|
},
|
|
70
|
+
rbac: { permission: "repo.merge-policy.write" },
|
|
68
71
|
});
|
|
69
72
|
|
|
70
73
|
const updateRepo = route({
|
|
@@ -85,9 +88,14 @@ const updateRepo = route({
|
|
|
85
88
|
}),
|
|
86
89
|
responses: {
|
|
87
90
|
200: { description: "Repo updated", schema: SwarmRepoSchema },
|
|
91
|
+
403: {
|
|
92
|
+
description: "Only the lead, the operator or a user can change allowMerge",
|
|
93
|
+
schema: z.object({ error: z.string() }),
|
|
94
|
+
},
|
|
88
95
|
404: { description: "Repo not found", schema: z.object({ error: z.string() }) },
|
|
89
96
|
409: { description: "Duplicate repo", schema: z.object({ error: z.string() }) },
|
|
90
97
|
},
|
|
98
|
+
rbac: { permission: "repo.merge-policy.write" },
|
|
91
99
|
});
|
|
92
100
|
|
|
93
101
|
const deleteRepo = route({
|
|
@@ -105,11 +113,26 @@ const deleteRepo = route({
|
|
|
105
113
|
|
|
106
114
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
107
115
|
|
|
116
|
+
/** The refusal text when the caller may not change allowMerge, or null when they may. */
|
|
117
|
+
async function allowMergeRefusal(
|
|
118
|
+
req: IncomingMessage,
|
|
119
|
+
myAgentId: string | undefined,
|
|
120
|
+
): Promise<string | null> {
|
|
121
|
+
const decision = can({
|
|
122
|
+
principal: await agentFirstPrincipal(req, myAgentId),
|
|
123
|
+
verb: "repo.merge-policy.write",
|
|
124
|
+
resource: { kind: "none" },
|
|
125
|
+
source: "http",
|
|
126
|
+
});
|
|
127
|
+
return decision.allow ? null : `Forbidden: ${decision.reason}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
108
130
|
export async function handleRepos(
|
|
109
131
|
req: IncomingMessage,
|
|
110
132
|
res: ServerResponse,
|
|
111
133
|
pathSegments: string[],
|
|
112
134
|
queryParams: URLSearchParams,
|
|
135
|
+
myAgentId: string | undefined,
|
|
113
136
|
): Promise<boolean> {
|
|
114
137
|
if (getRepo.match(req.method, pathSegments)) {
|
|
115
138
|
const parsed = await getRepo.parse(req, res, pathSegments, queryParams);
|
|
@@ -137,6 +160,13 @@ export async function handleRepos(
|
|
|
137
160
|
if (createRepo.match(req.method, pathSegments)) {
|
|
138
161
|
const parsed = await createRepo.parse(req, res, pathSegments, queryParams);
|
|
139
162
|
if (!parsed) return true;
|
|
163
|
+
if (changesAllowMerge(null, parsed.body.guidelines)) {
|
|
164
|
+
const refusal = await allowMergeRefusal(req, myAgentId);
|
|
165
|
+
if (refusal) {
|
|
166
|
+
jsonError(res, refusal, 403);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
140
170
|
try {
|
|
141
171
|
const repo = await createSwarmRepo({
|
|
142
172
|
url: parsed.body.url,
|
|
@@ -168,20 +198,30 @@ export async function handleRepos(
|
|
|
168
198
|
const parsed = await updateRepo.parse(req, res, pathSegments, queryParams);
|
|
169
199
|
if (!parsed) return true;
|
|
170
200
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
201
|
+
// The comparison with the stored allowMerge, the decision and the write share one
|
|
202
|
+
// transaction, so a stale edit cannot overwrite a merge-policy change committed after it.
|
|
203
|
+
const result = await updateSwarmRepoChecked(
|
|
204
|
+
parsed.params.id,
|
|
205
|
+
{
|
|
206
|
+
url: parsed.body.url,
|
|
207
|
+
name: parsed.body.name,
|
|
208
|
+
clonePath: parsed.body.clonePath,
|
|
209
|
+
defaultBranch: parsed.body.defaultBranch,
|
|
210
|
+
autoClone: parsed.body.autoClone,
|
|
211
|
+
hooks: parsed.body.hooks,
|
|
212
|
+
guidelines: parsed.body.guidelines,
|
|
213
|
+
},
|
|
214
|
+
() => allowMergeRefusal(req, myAgentId),
|
|
215
|
+
);
|
|
216
|
+
if (result.kind === "refused") {
|
|
217
|
+
jsonError(res, result.refusal, 403);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
if (result.kind === "not-found") {
|
|
181
221
|
jsonError(res, "Repo not found", 404);
|
|
182
222
|
return true;
|
|
183
223
|
}
|
|
184
|
-
json(res,
|
|
224
|
+
json(res, result.repo);
|
|
185
225
|
} catch (error) {
|
|
186
226
|
const msg = (error as Error).message;
|
|
187
227
|
if (msg.includes("UNIQUE constraint")) {
|
|
@@ -20,3 +20,22 @@ export async function requestPrincipal(
|
|
|
20
20
|
const agent = await getAgentById(myAgentId);
|
|
21
21
|
return { kind: "agent", agentId: myAgentId, isLead: agent?.isLead ?? false };
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The principal of a keyed HTTP request that may act as an agent. Workers share the swarm API
|
|
26
|
+
* key, so the key alone authenticates as the operator. An X-Agent-ID, or an `aseph_` session
|
|
27
|
+
* token, names the agent on top of it, and its lead flag is read live. A user token stays the
|
|
28
|
+
* user. A request with no agent identity is the operator, as for the dashboard and the runner.
|
|
29
|
+
* Unlike `requestPrincipal`, an agent identity wins over the operator key.
|
|
30
|
+
*/
|
|
31
|
+
export async function agentFirstPrincipal(
|
|
32
|
+
req: IncomingMessage,
|
|
33
|
+
myAgentId: string | undefined,
|
|
34
|
+
): Promise<RbacPrincipal> {
|
|
35
|
+
const auth = getRequestAuth(req);
|
|
36
|
+
if (auth?.kind === "user") return { kind: "user", userId: auth.userId };
|
|
37
|
+
const agentId = auth?.kind === "agent" ? auth.agentId : myAgentId;
|
|
38
|
+
if (!agentId) return { kind: "operator" };
|
|
39
|
+
const agent = await getAgentById(agentId);
|
|
40
|
+
return { kind: "agent", agentId, isLead: agent?.isLead === true };
|
|
41
|
+
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
SessionCostModelBreakdown,
|
|
6
6
|
SessionCostSource,
|
|
7
7
|
} from "../types";
|
|
8
|
+
import { AMP_ESTIMATE_MODELS, ampBillsCacheWrites, resolveAmpModel } from "../utils/amp-models";
|
|
8
9
|
|
|
9
10
|
const PER_MILLION = 1_000_000;
|
|
10
11
|
|
|
@@ -121,6 +122,52 @@ function unpricedResult(
|
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* An amp row with no per-model usage means the thread export failed, and the
|
|
127
|
+
* adapter sent the stream's token totals under the requested mode or pin.
|
|
128
|
+
* Recording $0 would let the session skip budget admission, so the totals are
|
|
129
|
+
* priced at the pin, or at the model the mode is known to run, falling back to
|
|
130
|
+
* the `medium` model when the pin has no rates. The row is tagged `estimated`.
|
|
131
|
+
*/
|
|
132
|
+
async function estimateAmpCost(
|
|
133
|
+
input: SessionCostRecomputeInput,
|
|
134
|
+
lookupRate: PricingRateLookup,
|
|
135
|
+
): Promise<SessionCostRecomputeResult> {
|
|
136
|
+
let selection: ReturnType<typeof resolveAmpModel> | undefined;
|
|
137
|
+
try {
|
|
138
|
+
selection = resolveAmpModel(input.model);
|
|
139
|
+
} catch {
|
|
140
|
+
selection = undefined;
|
|
141
|
+
}
|
|
142
|
+
const candidates = [
|
|
143
|
+
...new Set([
|
|
144
|
+
selection?.pin,
|
|
145
|
+
selection ? AMP_ESTIMATE_MODELS[selection.baseMode] : undefined,
|
|
146
|
+
AMP_ESTIMATE_MODELS.medium,
|
|
147
|
+
]),
|
|
148
|
+
].filter((model): model is string => !!model);
|
|
149
|
+
for (const model of candidates) {
|
|
150
|
+
// Same rule as the export parser: only Anthropic bills cache creation as writes.
|
|
151
|
+
const writes = ampBillsCacheWrites(model);
|
|
152
|
+
const costUsd = await priceModel(
|
|
153
|
+
"amp",
|
|
154
|
+
{
|
|
155
|
+
model,
|
|
156
|
+
inputTokens: input.inputTokens + (writes ? 0 : input.cacheWriteTokens),
|
|
157
|
+
outputTokens: input.outputTokens,
|
|
158
|
+
cacheReadTokens: input.cacheReadTokens,
|
|
159
|
+
cacheWriteTokens: writes ? input.cacheWriteTokens : 0,
|
|
160
|
+
},
|
|
161
|
+
null,
|
|
162
|
+
input.atEpochMs,
|
|
163
|
+
lookupRate,
|
|
164
|
+
);
|
|
165
|
+
if (costUsd != null)
|
|
166
|
+
return { totalCostUsd: costUsd, costSource: "estimated", modelBreakdown: undefined };
|
|
167
|
+
}
|
|
168
|
+
return unpricedResult(input, undefined);
|
|
169
|
+
}
|
|
170
|
+
|
|
124
171
|
export async function recomputeSessionCost(
|
|
125
172
|
input: SessionCostRecomputeInput,
|
|
126
173
|
lookupRate: PricingRateLookup,
|
|
@@ -135,6 +182,23 @@ export async function recomputeSessionCost(
|
|
|
135
182
|
};
|
|
136
183
|
}
|
|
137
184
|
|
|
185
|
+
// Amp's own recorded cost (`amp threads usage`) is what it billed, including
|
|
186
|
+
// subagent threads the token counts miss. The adapter sends it only when
|
|
187
|
+
// every request was billed through Amp, so it wins over the token price,
|
|
188
|
+
// which still fills the per-model breakdown.
|
|
189
|
+
if (input.provider === "amp" && input.harnessCostUsd > 0) {
|
|
190
|
+
const priced = await recomputeSessionCost({ ...input, harnessCostUsd: 0 }, lookupRate);
|
|
191
|
+
return {
|
|
192
|
+
totalCostUsd: input.harnessCostUsd,
|
|
193
|
+
costSource: "harness",
|
|
194
|
+
modelBreakdown: priced.modelBreakdown,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// amp's top-level model is the mode or pin it was asked for; only the thread
|
|
199
|
+
// export's per-model usage says what ran and how its cache tokens bill.
|
|
200
|
+
if (input.provider === "amp" && !modelUsageEntries) return estimateAmpCost(input, lookupRate);
|
|
201
|
+
|
|
138
202
|
const split = cacheWriteSplit(input);
|
|
139
203
|
const usages: SessionCostModelUsageInput[] = modelUsageEntries
|
|
140
204
|
? modelUsageEntries
|
package/src/http/session-data.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getSessionCostsFiltered,
|
|
14
14
|
getSessionLogsByTaskId,
|
|
15
15
|
getTaskById,
|
|
16
|
+
refreshActiveSessionOnActivity,
|
|
16
17
|
} from "../be/db";
|
|
17
18
|
import { recordSessionCost } from "../otel";
|
|
18
19
|
import { incrementServerSessionsProcessed } from "../server-runtime-counters";
|
|
@@ -319,6 +320,16 @@ export async function handleSessionData(
|
|
|
319
320
|
cli: parsed.body.cli || "claude",
|
|
320
321
|
lines: parsed.body.lines,
|
|
321
322
|
});
|
|
323
|
+
// Every harness streams its output through this endpoint, so a task that
|
|
324
|
+
// is producing output keeps its session alive here instead of per
|
|
325
|
+
// provider. Best-effort: a liveness write must never fail log storage.
|
|
326
|
+
if (parsed.body.taskId) {
|
|
327
|
+
try {
|
|
328
|
+
await refreshActiveSessionOnActivity(parsed.body.taskId);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.warn("[HTTP] Failed to refresh session liveness from session logs:", error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
322
333
|
createSessionLogsRoute.respond(res, 201, {
|
|
323
334
|
success: true,
|
|
324
335
|
count: parsed.body.lines.length,
|
package/src/http/tasks.ts
CHANGED
|
@@ -888,7 +888,15 @@ export async function handleTasks(
|
|
|
888
888
|
let requestedByUserId = trustedUserId ?? undefined;
|
|
889
889
|
const trustBodyRequestedByUserId = process.env.TRUST_BODY_REQUESTED_BY_USER_ID !== "false";
|
|
890
890
|
if (trustBodyRequestedByUserId && !requestedByUserId && parsed.body.requestedByUserId) {
|
|
891
|
-
|
|
891
|
+
// A worker on the shared key is not the operator: its X-Agent-ID names it, so the body
|
|
892
|
+
// hint is dropped like it is when the worker has a requester of its own.
|
|
893
|
+
const mayAssign = can({
|
|
894
|
+
principal: await resolveTaskWritePrincipal(req, myAgentId),
|
|
895
|
+
verb: "task.requester.assign",
|
|
896
|
+
resource: { kind: "none" },
|
|
897
|
+
source: "http",
|
|
898
|
+
}).allow;
|
|
899
|
+
const candidate = mayAssign ? await findUserById(parsed.body.requestedByUserId) : null;
|
|
892
900
|
if (candidate) requestedByUserId = candidate.id;
|
|
893
901
|
}
|
|
894
902
|
|