@desplega.ai/agent-swarm 1.131.0 → 1.133.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/README.md +2 -2
- package/dist/{actions-w53mbns9.js → actions-cskpayd7.js} +11 -9
- package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
- package/dist/{app-m9gwqsqv.js → app-ang77nbb.js} +8 -6
- package/dist/{assistant-bp65m6f3.js → assistant-b4exebdw.js} +16 -13
- package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
- package/dist/{boot-reembed-62p0at2k.js → boot-reembed-ps2aftpb.js} +9 -7
- package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-znyk3nxb.js} +10 -8
- package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
- package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-3ct8kgxw.js} +13 -9
- package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-akx4ntf2.js} +2 -2
- package/dist/{cli-t1xze5qh.js → cli-13w2hwkh.js} +1 -1
- package/dist/{cli-efxfnq6q.js → cli-1532szb1.js} +8 -6
- package/dist/{cli-d2tfkcga.js → cli-23d3rseb.js} +33 -17
- package/dist/{cli-nm5gtp1x.js → cli-2a75z9c5.js} +196 -7
- package/dist/{cli-fywms69x.js → cli-2t30rj0d.js} +1 -1
- package/dist/{cli-cvgjwnh1.js → cli-3c3tdgab.js} +10 -10
- package/dist/{cli-pm6xbh84.js → cli-3zzxa9jz.js} +46 -16
- package/dist/{cli-cdeg08wx.js → cli-4k6233gc.js} +1 -1
- package/dist/{cli-t55yw3a1.js → cli-5p2tz05r.js} +2 -2
- package/dist/{cli-0rcyq6d9.js → cli-5z2yhzvd.js} +125 -47
- package/dist/{cli-3kf51j15.js → cli-6zfq31nj.js} +9 -3
- package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
- package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
- package/dist/{cli-72q8m8gk.js → cli-9nfc2es8.js} +6 -6
- package/dist/{cli-5ze9rv97.js → cli-9ta3pwms.js} +11 -4
- package/dist/{cli-78xwvkhj.js → cli-avr0ns7z.js} +2 -2
- package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
- package/dist/{cli-2q72qz2s.js → cli-dgk20ft7.js} +15 -6
- package/dist/cli-et1bbwqf.js +13 -0
- package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
- package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
- package/dist/{cli-e7ckcg4v.js → cli-ftwpv451.js} +170 -387
- package/dist/{cli-g6b8x1h8.js → cli-g854s2dw.js} +1 -1
- package/dist/{cli-5kx36evn.js → cli-ggmdxeqa.js} +13 -8
- package/dist/cli-hkwfe3qj.js +530 -0
- package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
- package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
- package/dist/{cli-5v4rw6yd.js → cli-pvzqhbs9.js} +2 -2
- package/dist/cli-pzq5rjym.js +15 -0
- package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
- package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
- package/dist/cli-qys3rhhz.js +27 -0
- package/dist/{cli-rzbpmxdr.js → cli-r6tpm37h.js} +2 -2
- package/dist/{cli-qv61hp37.js → cli-rch80c76.js} +2 -2
- package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
- package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
- package/dist/{cli-psh3c4va.js → cli-sfd9v7sj.js} +1 -1
- package/dist/{cli-70xqj0z3.js → cli-sh3ytsyj.js} +49 -39
- package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
- package/dist/{cli-d3x3x1c9.js → cli-xdb4mpnp.js} +2 -2
- package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
- package/dist/{cli-yw8vghx2.js → cli-xv8t8p3c.js} +65 -32
- package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
- package/dist/{cli-t76vw5db.js → cli-y08jvxzx.js} +601 -140
- package/dist/{cli-a4jzdpa3.js → cli-zfbf3srj.js} +650 -245
- package/dist/{cli-a40rnf71.js → cli-zmq89k34.js} +6 -6
- package/dist/{cli-qmdnhpej.js → cli-zr9q011v.js} +23 -13
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-d24q75wk.js → codex-adapter-bef93pq4.js} +9 -7
- package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
- package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-5pkxys7x.js} +9 -7
- package/dist/{commands-c3nrc5hg.js → commands-e23s1bff.js} +9 -7
- package/dist/{db-nt7fg3tw.js → db-m3nabatn.js} +17 -5
- package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
- package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
- package/dist/{handlers-fkr6swat.js → handlers-akazedvr.js} +16 -13
- package/dist/{hook-77wpvnjc.js → hook-ze4cpf0v.js} +55 -41
- package/dist/{hook-g019yehy.js → hook-zfqyyenw.js} +10 -7
- package/dist/{http-jewj0syk.js → http-sja0n6kw.js} +730 -150
- package/dist/index-0gr8vd6k.js +39 -0
- package/dist/{index-7n2xpbd4.js → index-354vkhdv.js} +154 -74
- package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
- package/dist/{index-hfvggd0z.js → index-95t3cqq5.js} +16 -14
- package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
- package/dist/{index-6gm5he8h.js → index-vpn36mma.js} +15 -13
- package/dist/{keepalive-j0fjvqfd.js → keepalive-3sp3meqj.js} +16 -10
- package/dist/{lead-1n8x30xe.js → lead-mefeewkf.js} +39 -36
- package/dist/{maintenance-gvev11sa.js → maintenance-8tbrbjfg.js} +10 -8
- package/dist/mistral-conversations-00zrp7g4.js +717 -0
- package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
- package/dist/{onboard-pvm5b38w.js → onboard-mt2tgyjg.js} +7 -7
- package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
- package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
- package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
- package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
- package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
- package/dist/{otel-impl-p6jvgd9c.js → otel-impl-z6pxt3j9.js} +224 -9
- package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-btvqmj58.js} +1138 -408
- package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-kyq8cezh.js} +9 -7
- package/dist/{rbac-roles-cr80g96t.js → rbac-roles-3ryfrfna.js} +8 -6
- package/dist/{rbac-roles-30pspvy7.js → rbac-roles-6qfvbee1.js} +9 -7
- package/dist/{render-v2-xm457ng1.js → render-v2-p61ppjey.js} +8 -6
- package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-j56bbj4y.js} +8 -6
- package/dist/{setup-nytnmwwr.js → setup-tpr6c1kn.js} +2 -2
- package/dist/{worker-msytr2cw.js → worker-9w1v0mbb.js} +39 -36
- package/openapi.json +542 -16
- package/package.json +6 -6
- package/src/be/budget-refusal-notify.ts +14 -7
- package/src/be/db-queries/task-artifacts.ts +182 -0
- package/src/be/db.ts +783 -169
- package/src/be/events.ts +5 -0
- package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
- package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
- package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
- package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
- package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
- package/src/be/migrations/136_task_requester_provenance.sql +37 -0
- package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
- package/src/be/multi-runtime.ts +430 -0
- package/src/be/scripts/db.ts +43 -1
- package/src/be/scripts/extract-schema.ts +3 -1
- package/src/be/scripts/retention.ts +164 -0
- package/src/be/scripts/run-saved.ts +16 -1
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/steering.ts +1 -1
- package/src/be/swarm-config-guard.ts +18 -0
- package/src/commands/credential-wait.ts +54 -0
- package/src/commands/onboard/steps/core-credentials.tsx +1 -1
- package/src/commands/onboard/steps/custom-templates.tsx +1 -1
- package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
- package/src/commands/onboard/steps/health-check.tsx +1 -1
- package/src/commands/onboard/steps/start.tsx +1 -1
- package/src/commands/profile-sync-audit.ts +56 -0
- package/src/commands/profile-sync.ts +287 -15
- package/src/commands/provider-credentials.ts +46 -19
- package/src/commands/runner.ts +196 -33
- package/src/commands/templates.ts +22 -0
- package/src/github/handlers.ts +55 -9
- package/src/heartbeat/heartbeat.ts +71 -12
- package/src/hooks/hook.ts +49 -33
- package/src/http/active-sessions.ts +11 -1
- package/src/http/agents.ts +261 -30
- package/src/http/all-routes.ts +1 -0
- package/src/http/config.ts +13 -2
- package/src/http/core.ts +102 -4
- package/src/http/db-query-bounded.ts +313 -0
- package/src/http/db-query-shared.ts +249 -0
- package/src/http/db-query.ts +54 -42
- package/src/http/events.ts +2 -0
- package/src/http/index.ts +27 -2
- package/src/http/mcp-bridge.ts +37 -3
- package/src/http/mcp.ts +12 -0
- package/src/http/memory.ts +13 -2
- package/src/http/openapi.ts +1 -0
- package/src/http/poll.ts +70 -27
- package/src/http/route-def.ts +43 -9
- package/src/http/schedules.ts +2 -2
- package/src/http/scripts.ts +31 -4
- package/src/http/session-data.ts +44 -0
- package/src/http/tasks.ts +2 -2
- package/src/http/users.ts +74 -52
- package/src/http/webhooks.ts +5 -2
- package/src/http/workflows.ts +1 -4
- package/src/http/x.ts +14 -0
- package/src/integrations/kapso/inbound.ts +1 -1
- package/src/jira/outbound.ts +39 -8
- package/src/jira/webhook-lifecycle.ts +13 -2
- package/src/linear/outbound.ts +43 -10
- package/src/oauth/keepalive.ts +13 -2
- package/src/prompts/base-prompt.ts +1 -1
- package/src/prompts/session-templates.ts +45 -7
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/claude-managed-models.ts +34 -10
- package/src/providers/codex-adapter.ts +6 -0
- package/src/providers/opencode-adapter.ts +6 -0
- package/src/providers/pi-mono-adapter.ts +8 -0
- package/src/providers/pricing-sources.md +24 -0
- package/src/queue-stall-alarm.ts +216 -0
- package/src/scheduler/scheduler.ts +7 -0
- package/src/script-workflows/executor.ts +2 -1
- package/src/scripts-runtime/executors/native.ts +2 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/loader.ts +5 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/tests/budget-claim-gate.test.ts +20 -0
- package/src/tests/claude-adapter.test.ts +32 -0
- package/src/tests/claude-managed-adapter.test.ts +38 -7
- package/src/tests/codex-adapter.test.ts +30 -0
- package/src/tests/create-task-validation.test.ts +128 -0
- package/src/tests/credential-wait.test.ts +67 -1
- package/src/tests/db-query.test.ts +711 -2
- package/src/tests/http-api-integration.test.ts +81 -0
- package/src/tests/http-users.test.ts +166 -1
- package/src/tests/identity-field-budget.test.ts +176 -0
- package/src/tests/mcp-tools-user.test.ts +34 -0
- package/src/tests/mcp-user-route.test.ts +17 -0
- package/src/tests/metrics-http.test.ts +44 -0
- package/src/tests/multi-runtime-registration.test.ts +2703 -0
- package/src/tests/openapi-response-contract.test.ts +24 -0
- package/src/tests/openapi-route-headers.test.ts +83 -0
- package/src/tests/opencode-adapter.test.ts +40 -0
- package/src/tests/outbound-listener-rejection.test.ts +138 -0
- package/src/tests/pi-mono-adapter.test.ts +98 -0
- package/src/tests/profile-sync-warning.test.ts +277 -0
- package/src/tests/profile-sync.test.ts +232 -6
- package/src/tests/prompt-template-session.test.ts +32 -3
- package/src/tests/queue-stall-alarm.test.ts +226 -0
- package/src/tests/requester-comms.test.ts +32 -0
- package/src/tests/runner-requester-profile.test.ts +38 -0
- package/src/tests/sandboxed-process.test.ts +45 -0
- package/src/tests/schedule-target-type.test.ts +38 -0
- package/src/tests/script-connections.test.ts +16 -3
- package/src/tests/scripts-external-api.test.ts +8 -3
- package/src/tests/scripts-http.test.ts +12 -0
- package/src/tests/scripts-retention.test.ts +384 -0
- package/src/tests/scripts-runtime-identity.test.ts +375 -0
- package/src/tests/scripts-runtime.test.ts +4 -3
- package/src/tests/sdk-allowlist.test.ts +47 -0
- package/src/tests/send-task-requested-by.test.ts +9 -0
- package/src/tests/session-costs.test.ts +506 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
- package/src/tests/steering-core.test.ts +28 -0
- package/src/tests/task-artifacts.test.ts +386 -0
- package/src/tests/task-attachments-schema.test.ts +17 -1
- package/src/tests/task-completion-idempotency.test.ts +57 -0
- package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
- package/src/tests/update-profile-api.test.ts +67 -39
- package/src/tests/update-profile-auth.test.ts +48 -4
- package/src/tests/user-attribution-migration.test.ts +54 -0
- package/src/tests/user-identity.test.ts +72 -0
- package/src/tests/workflow-async-v2.test.ts +39 -1
- package/src/tests/workflow-http-v2.test.ts +3 -2
- package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
- package/src/tests/workflow-retry-v2.test.ts +45 -1
- package/src/tests/workflow-swarm-script.test.ts +14 -5
- package/src/tools/db-query.ts +18 -8
- package/src/tools/get-task-details.ts +24 -3
- package/src/tools/manage-user.ts +36 -3
- package/src/tools/poll-task.ts +93 -14
- package/src/tools/script-common.ts +5 -0
- package/src/tools/send-task.ts +1 -4
- package/src/tools/store-progress.ts +13 -2
- package/src/tools/swarm-config/delete-config.ts +9 -0
- package/src/tools/swarm-config/set-config.ts +3 -2
- package/src/tools/task-action.ts +30 -0
- package/src/tools/task-tool-ctx.ts +8 -1
- package/src/tools/update-profile.ts +16 -21
- package/src/tools/utils.ts +5 -0
- package/src/tools/workflows/trigger-workflow.ts +7 -1
- package/src/types.ts +165 -1
- package/src/utils/github-pull-request.ts +76 -0
- package/src/utils/identity-field-budget.ts +73 -0
- package/src/utils/multi-runtime.ts +20 -0
- package/src/utils/requester-comms.ts +24 -0
- package/src/utils/sandboxed-process.ts +40 -14
- package/src/workflows/engine.ts +22 -2
- package/src/workflows/executors/swarm-script.ts +24 -1
- package/src/workflows/resume.ts +19 -11
- package/src/workflows/retry-poller.ts +1 -0
- package/src/workflows/triggers.ts +4 -0
- package/templates/schedules/daily-compounding-reflection/content.md +12 -3
- package/templates/skills/db-query-guidance/config.json +14 -0
- package/templates/skills/db-query-guidance/content.md +57 -0
- package/dist/cli-3te4jdkm.js +0 -225
- package/dist/cli-bmze986d.js +0 -7
- package/dist/cli-kh3ba2zf.js +0 -607
- package/dist/index-hp1kdbjj.js +0 -36
- package/dist/mistral-conversations-81fwav0j.js +0 -30578
- package/dist/otel-nqa12wf3.js +0 -3149
- package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
package/src/http/agents.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { ensure } from "@desplega.ai/business-use";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import {
|
|
5
|
+
computeContentHash,
|
|
5
6
|
createAgent,
|
|
6
7
|
deleteSwarmConfigByKey,
|
|
7
8
|
getAgentById,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
resetEmptyPollCount,
|
|
14
15
|
setAgentHarnessProvider,
|
|
15
16
|
updateAgentActivity,
|
|
17
|
+
updateAgentCredentialMissing,
|
|
16
18
|
updateAgentCredentialState,
|
|
17
19
|
updateAgentCredStatus,
|
|
18
20
|
updateAgentMaxTasks,
|
|
@@ -22,10 +24,21 @@ import {
|
|
|
22
24
|
updateAgentStatus,
|
|
23
25
|
upsertSwarmConfig,
|
|
24
26
|
} from "../be/db";
|
|
27
|
+
import { createEvent } from "../be/events";
|
|
28
|
+
import {
|
|
29
|
+
getRuntimeInstanceById,
|
|
30
|
+
listRuntimeInstancesForAgent,
|
|
31
|
+
reconcileAgentMaxTasksPolicy,
|
|
32
|
+
reconcileAgentStatusFromRuntimes,
|
|
33
|
+
runtimeStaleThresholdMinutes,
|
|
34
|
+
setRuntimeCredentialReady,
|
|
35
|
+
upsertRuntimeInstance,
|
|
36
|
+
} from "../be/multi-runtime";
|
|
25
37
|
import { reasoningCapability } from "../providers/reasoning-effort";
|
|
26
38
|
import { ALL_CAPABILITIES, getEnabledCapabilities } from "../server";
|
|
27
39
|
import { telemetry } from "../telemetry";
|
|
28
40
|
import {
|
|
41
|
+
type Agent,
|
|
29
42
|
AgentAvatarSchema,
|
|
30
43
|
AgentCredStatusSchema,
|
|
31
44
|
AgentLatestModelSchema,
|
|
@@ -35,11 +48,24 @@ import {
|
|
|
35
48
|
type ProviderName,
|
|
36
49
|
ProviderNameSchema,
|
|
37
50
|
ReasoningEffortSchema,
|
|
51
|
+
RuntimeInstanceSchema,
|
|
38
52
|
} from "../types";
|
|
39
53
|
import { MAX_PROFILE_FILE_LENGTH } from "../utils/constants";
|
|
40
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
type BudgetedIdentityField,
|
|
56
|
+
IDENTITY_FIELD_BUDGETS,
|
|
57
|
+
IdentityFieldBudgetError,
|
|
58
|
+
} from "../utils/identity-field-budget";
|
|
59
|
+
import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
|
|
60
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
61
|
+
import { route, runtimeInstanceHeader } from "./route-def";
|
|
41
62
|
import { agentWithCapacity, json, jsonError } from "./utils";
|
|
42
63
|
|
|
64
|
+
function singleHeaderValue(req: IncomingMessage, name: string): string | undefined {
|
|
65
|
+
const raw = req.headers[name];
|
|
66
|
+
return Array.isArray(raw) ? raw[0] : raw;
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
44
70
|
|
|
45
71
|
/** Mirrors `CAPABILITIES_T` (src/server.ts) — the server's registered MCP tool-group flags. */
|
|
@@ -103,6 +129,13 @@ const registerAgent = route({
|
|
|
103
129
|
* the canonical list — unknown values reject the request with 400.
|
|
104
130
|
*/
|
|
105
131
|
harness_provider: ProviderNameSchema.optional(),
|
|
132
|
+
/**
|
|
133
|
+
* Per-process runtime identity; `X-Agent-ID` remains the logical agent.
|
|
134
|
+
* Ignored when MULTI_RUNTIME_ENABLED is off, required when it is on —
|
|
135
|
+
* schema-optional so older workers still parse, with the handler
|
|
136
|
+
* enforcing the mode-dependent requirement.
|
|
137
|
+
*/
|
|
138
|
+
runtimeInstanceId: z.string().min(1).optional(),
|
|
106
139
|
}),
|
|
107
140
|
responses: {
|
|
108
141
|
200: {
|
|
@@ -216,6 +249,41 @@ const getAgentSetupScript = route({
|
|
|
216
249
|
},
|
|
217
250
|
});
|
|
218
251
|
|
|
252
|
+
/** Operator view of a runtime instance: internal `metadata` omitted, server-derived `isLive` added. */
|
|
253
|
+
const AgentRuntimeInstanceSchema = RuntimeInstanceSchema.omit({ metadata: true }).extend({
|
|
254
|
+
isLive: z.boolean(),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const listAgentRuntimeInstances = route({
|
|
258
|
+
method: "get",
|
|
259
|
+
path: "/api/agents/{id}/runtime-instances",
|
|
260
|
+
pattern: ["api", "agents", null, "runtime-instances"],
|
|
261
|
+
summary: "List runtime instances serving an agent",
|
|
262
|
+
description:
|
|
263
|
+
"Read-only view of the worker processes currently registered for a logical agent. Rows exist only for multi-runtime registrations (MULTI_RUNTIME_ENABLED), so the list is empty in the default configuration. `isLive` combines `status` with `lastSeenAt` freshness against the server's staleness cutoff (`staleThresholdMinutes`); `reportedSlots` is each process's self-reported capacity, distinct from the agent's logical `maxTasks` policy.",
|
|
264
|
+
tags: ["Agents"],
|
|
265
|
+
params: z.object({ id: z.string() }),
|
|
266
|
+
responses: {
|
|
267
|
+
200: {
|
|
268
|
+
description: "Runtime instances for the agent (empty when none are registered)",
|
|
269
|
+
schema: z.object({
|
|
270
|
+
runtimeInstances: z.array(AgentRuntimeInstanceSchema),
|
|
271
|
+
staleThresholdMinutes: z.number().int(),
|
|
272
|
+
}),
|
|
273
|
+
},
|
|
274
|
+
404: { description: "Agent not found" },
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const ProfileSyncRejectionSchema = z.object({
|
|
279
|
+
field: z.enum(["soulMd", "identityMd", "claudeMd", "toolsMd"]),
|
|
280
|
+
diskSize: z.number().int(),
|
|
281
|
+
dbSize: z.number().int(),
|
|
282
|
+
budget: z.number().int(),
|
|
283
|
+
delta: z.number().int(),
|
|
284
|
+
reason: z.string(),
|
|
285
|
+
});
|
|
286
|
+
|
|
219
287
|
const updateAgentProfileRoute = route({
|
|
220
288
|
method: "put",
|
|
221
289
|
path: "/api/agents/{id}/profile",
|
|
@@ -227,11 +295,11 @@ const updateAgentProfileRoute = route({
|
|
|
227
295
|
role: z.string().max(100).optional(),
|
|
228
296
|
description: z.string().optional(),
|
|
229
297
|
capabilities: z.array(z.string()).optional(),
|
|
230
|
-
claudeMd: z.string().
|
|
231
|
-
soulMd: z.string().
|
|
232
|
-
identityMd: z.string().
|
|
298
|
+
claudeMd: z.string().optional(),
|
|
299
|
+
soulMd: z.string().optional(),
|
|
300
|
+
identityMd: z.string().optional(),
|
|
233
301
|
setupScript: z.string().max(MAX_PROFILE_FILE_LENGTH).optional(),
|
|
234
|
-
toolsMd: z.string().
|
|
302
|
+
toolsMd: z.string().optional(),
|
|
235
303
|
heartbeatMd: z.string().max(MAX_PROFILE_FILE_LENGTH).optional(),
|
|
236
304
|
/** `null` resets to the deterministic fallback; omit the key to leave untouched. */
|
|
237
305
|
avatar: AgentAvatarSchema.nullable().optional(),
|
|
@@ -241,7 +309,13 @@ const updateAgentProfileRoute = route({
|
|
|
241
309
|
}),
|
|
242
310
|
responses: {
|
|
243
311
|
200: { description: "Profile updated", schema: AgentWithCapacitySchema },
|
|
244
|
-
400: {
|
|
312
|
+
400: {
|
|
313
|
+
description: "Validation or identity-field budget error",
|
|
314
|
+
schema: z.object({
|
|
315
|
+
error: z.string(),
|
|
316
|
+
profileSyncRejection: ProfileSyncRejectionSchema.optional(),
|
|
317
|
+
}),
|
|
318
|
+
},
|
|
245
319
|
404: { description: "Agent not found" },
|
|
246
320
|
},
|
|
247
321
|
});
|
|
@@ -301,12 +375,14 @@ const updateAgentCredentialStatusRoute = route({
|
|
|
301
375
|
summary: "Worker self-report of credential readiness (Phase 3 boot loop)",
|
|
302
376
|
tags: ["Agents"],
|
|
303
377
|
params: z.object({ id: z.string() }),
|
|
378
|
+
headers: runtimeInstanceHeader("report credential readiness"),
|
|
304
379
|
body: credentialStatusBody,
|
|
305
380
|
responses: {
|
|
306
381
|
200: {
|
|
307
382
|
description: "State updated; returns the agent row.",
|
|
308
383
|
schema: AgentWithCapacitySchema,
|
|
309
384
|
},
|
|
385
|
+
400: { description: "Missing X-Runtime-Instance-ID in multi-runtime mode" },
|
|
310
386
|
404: { description: "Agent not found" },
|
|
311
387
|
},
|
|
312
388
|
});
|
|
@@ -354,6 +430,25 @@ export async function handleAgentRegister(
|
|
|
354
430
|
if (!parsed) return true;
|
|
355
431
|
|
|
356
432
|
const agentId = myAgentId || crypto.randomUUID();
|
|
433
|
+
// Read once so one mode applies consistently even if a config reload
|
|
434
|
+
// flips the flag mid-request.
|
|
435
|
+
const multiRuntime = isMultiRuntimeEnabled();
|
|
436
|
+
|
|
437
|
+
if (multiRuntime && parsed.body.runtimeInstanceId) {
|
|
438
|
+
// Runtime ownership is permanent: a runtime id already serving another
|
|
439
|
+
// agent must not be moved. `upsertRuntimeInstance` enforces this in SQL
|
|
440
|
+
// too; rejecting here keeps the failure legible and pre-mutation.
|
|
441
|
+
const existingRuntime = getRuntimeInstanceById(parsed.body.runtimeInstanceId);
|
|
442
|
+
if (existingRuntime && existingRuntime.agentId !== agentId) {
|
|
443
|
+
jsonError(res, "runtimeInstanceId is already registered to another agent", 400);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
} else if (multiRuntime) {
|
|
447
|
+
// Enforced here rather than in the schema so the field stays optional
|
|
448
|
+
// for workers running against a server with the flag off.
|
|
449
|
+
jsonError(res, "runtimeInstanceId is required when multi-runtime mode is enabled", 400);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
357
452
|
|
|
358
453
|
const result = getDb().transaction(() => {
|
|
359
454
|
const existingAgent = getAgentById(agentId);
|
|
@@ -361,7 +456,15 @@ export async function handleAgentRegister(
|
|
|
361
456
|
if (existingAgent.status === "offline") {
|
|
362
457
|
updateAgentStatus(existingAgent.id, "idle");
|
|
363
458
|
}
|
|
364
|
-
if (
|
|
459
|
+
if (multiRuntime) {
|
|
460
|
+
// body.maxTasks is this runtime's own capacity, recorded below; it
|
|
461
|
+
// must not redefine the logical policy, or two runtimes serving one
|
|
462
|
+
// agent would race to overwrite it.
|
|
463
|
+
reconcileAgentMaxTasksPolicy(existingAgent.id);
|
|
464
|
+
} else if (
|
|
465
|
+
parsed.body.maxTasks !== undefined &&
|
|
466
|
+
parsed.body.maxTasks !== existingAgent.maxTasks
|
|
467
|
+
) {
|
|
365
468
|
updateAgentMaxTasks(existingAgent.id, parsed.body.maxTasks);
|
|
366
469
|
}
|
|
367
470
|
if (parsed.body.provider && parsed.body.provider !== existingAgent.provider) {
|
|
@@ -379,6 +482,16 @@ export async function handleAgentRegister(
|
|
|
379
482
|
setAgentHarnessProvider(existingAgent.id, parsed.body.harness_provider);
|
|
380
483
|
}
|
|
381
484
|
resetEmptyPollCount(existingAgent.id);
|
|
485
|
+
if (multiRuntime && parsed.body.runtimeInstanceId) {
|
|
486
|
+
upsertRuntimeInstance({
|
|
487
|
+
id: parsed.body.runtimeInstanceId,
|
|
488
|
+
agentId: existingAgent.id,
|
|
489
|
+
reportedSlots: parsed.body.maxTasks ?? 1,
|
|
490
|
+
});
|
|
491
|
+
// A new live runtime can lift the agent out of waiting without
|
|
492
|
+
// forcing idle over work another runtime is already doing.
|
|
493
|
+
reconcileAgentStatusFromRuntimes(existingAgent.id);
|
|
494
|
+
}
|
|
382
495
|
return { agent: getAgentById(agentId), created: false };
|
|
383
496
|
}
|
|
384
497
|
|
|
@@ -390,11 +503,29 @@ export async function handleAgentRegister(
|
|
|
390
503
|
description: parsed.body.description,
|
|
391
504
|
role: parsed.body.role,
|
|
392
505
|
capabilities: parsed.body.capabilities ?? [],
|
|
506
|
+
// The first registration establishes the logical policy — including
|
|
507
|
+
// role defaults such as a lead's two concurrent tasks. Later
|
|
508
|
+
// registrations cannot change it; reconcile below only seeds.
|
|
393
509
|
maxTasks: parsed.body.maxTasks ?? 1,
|
|
394
510
|
provider: parsed.body.provider,
|
|
395
511
|
harnessProvider: parsed.body.harness_provider ?? null,
|
|
396
512
|
});
|
|
397
513
|
|
|
514
|
+
if (multiRuntime) {
|
|
515
|
+
// Re-fetch below: this may adopt a policy row an operator wrote
|
|
516
|
+
// before the agent existed.
|
|
517
|
+
reconcileAgentMaxTasksPolicy(agent.id);
|
|
518
|
+
if (parsed.body.runtimeInstanceId) {
|
|
519
|
+
upsertRuntimeInstance({
|
|
520
|
+
id: parsed.body.runtimeInstanceId,
|
|
521
|
+
agentId: agent.id,
|
|
522
|
+
reportedSlots: parsed.body.maxTasks ?? 1,
|
|
523
|
+
});
|
|
524
|
+
reconcileAgentStatusFromRuntimes(agent.id);
|
|
525
|
+
}
|
|
526
|
+
return { agent: getAgentById(agent.id), created: true };
|
|
527
|
+
}
|
|
528
|
+
|
|
398
529
|
return { agent, created: true };
|
|
399
530
|
})();
|
|
400
531
|
|
|
@@ -506,6 +637,22 @@ export async function handleAgentsRest(
|
|
|
506
637
|
return true;
|
|
507
638
|
}
|
|
508
639
|
|
|
640
|
+
if (listAgentRuntimeInstances.match(req.method, pathSegments)) {
|
|
641
|
+
const parsed = await listAgentRuntimeInstances.parse(req, res, pathSegments, queryParams);
|
|
642
|
+
if (!parsed) return true;
|
|
643
|
+
if (!getAgentById(parsed.params.id)) {
|
|
644
|
+
jsonError(res, "Agent not found", 404);
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
listAgentRuntimeInstances.respond(res, 200, {
|
|
648
|
+
runtimeInstances: listRuntimeInstancesForAgent(parsed.params.id).map(
|
|
649
|
+
({ metadata: _metadata, ...instance }) => instance,
|
|
650
|
+
),
|
|
651
|
+
staleThresholdMinutes: runtimeStaleThresholdMinutes(),
|
|
652
|
+
});
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
|
|
509
656
|
if (updateAgentProfileRoute.match(req.method, pathSegments)) {
|
|
510
657
|
const parsed = await updateAgentProfileRoute.parse(req, res, pathSegments, queryParams);
|
|
511
658
|
if (!parsed) return true;
|
|
@@ -545,30 +692,92 @@ export async function handleAgentsRest(
|
|
|
545
692
|
}
|
|
546
693
|
: undefined;
|
|
547
694
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
695
|
+
let agent: Agent | null;
|
|
696
|
+
try {
|
|
697
|
+
agent = updateAgentProfile(
|
|
698
|
+
parsed.params.id,
|
|
699
|
+
{
|
|
700
|
+
role: body.role,
|
|
701
|
+
description: body.description,
|
|
702
|
+
capabilities: body.capabilities,
|
|
703
|
+
claudeMd: body.claudeMd,
|
|
704
|
+
soulMd: body.soulMd,
|
|
705
|
+
identityMd: body.identityMd,
|
|
706
|
+
setupScript: body.setupScript,
|
|
707
|
+
toolsMd: body.toolsMd,
|
|
708
|
+
heartbeatMd: body.heartbeatMd,
|
|
709
|
+
// Only include the key when the client sent it, so `null` (reset)
|
|
710
|
+
// is distinguishable from "not provided" (leave untouched).
|
|
711
|
+
...(body.avatar !== undefined ? { avatar: body.avatar } : {}),
|
|
712
|
+
},
|
|
713
|
+
versionMeta,
|
|
714
|
+
);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (error instanceof IdentityFieldBudgetError) {
|
|
717
|
+
if (
|
|
718
|
+
versionMeta?.changeSource === "self_edit" ||
|
|
719
|
+
versionMeta?.changeSource === "session_sync"
|
|
720
|
+
) {
|
|
721
|
+
try {
|
|
722
|
+
createEvent({
|
|
723
|
+
category: "system",
|
|
724
|
+
event: "system.profile_sync_rejected",
|
|
725
|
+
status: "error",
|
|
726
|
+
source: "api",
|
|
727
|
+
agentId: parsed.params.id,
|
|
728
|
+
data: {
|
|
729
|
+
...error.rejection,
|
|
730
|
+
dbHash: error.dbHash,
|
|
731
|
+
diskHash: error.diskHash,
|
|
732
|
+
changeSource: versionMeta.changeSource,
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
} catch (eventError) {
|
|
736
|
+
const message = eventError instanceof Error ? eventError.message : String(eventError);
|
|
737
|
+
console.error(
|
|
738
|
+
scrubSecrets(`[profile-sync] Failed to persist budget rejection event: ${message}`),
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
updateAgentProfileRoute.respond(res, 400, {
|
|
743
|
+
error: error.message,
|
|
744
|
+
profileSyncRejection: error.rejection,
|
|
745
|
+
});
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
566
750
|
|
|
567
751
|
if (!agent) {
|
|
568
752
|
jsonError(res, "Agent not found", 404);
|
|
569
753
|
return true;
|
|
570
754
|
}
|
|
571
755
|
|
|
756
|
+
if (versionMeta?.changeSource === "self_edit" || versionMeta?.changeSource === "session_sync") {
|
|
757
|
+
try {
|
|
758
|
+
for (const field of Object.keys(IDENTITY_FIELD_BUDGETS) as BudgetedIdentityField[]) {
|
|
759
|
+
if (body[field] === undefined) continue;
|
|
760
|
+
createEvent({
|
|
761
|
+
category: "system",
|
|
762
|
+
event: "system.profile_sync_reconciled",
|
|
763
|
+
status: "ok",
|
|
764
|
+
source: "api",
|
|
765
|
+
agentId: parsed.params.id,
|
|
766
|
+
data: {
|
|
767
|
+
field,
|
|
768
|
+
dbHash: computeContentHash(agent[field] ?? ""),
|
|
769
|
+
changeSource: versionMeta.changeSource,
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
} catch (eventError) {
|
|
774
|
+
const message = eventError instanceof Error ? eventError.message : String(eventError);
|
|
775
|
+
console.error(
|
|
776
|
+
scrubSecrets(`[profile-sync] Failed to persist reconciliation event: ${message}`),
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
572
781
|
updateAgentProfileRoute.respond(res, 200, agentWithCapacity(agent));
|
|
573
782
|
return true;
|
|
574
783
|
}
|
|
@@ -730,14 +939,36 @@ export async function handleAgentsRest(
|
|
|
730
939
|
jsonError(res, "Agent not found", 404);
|
|
731
940
|
return true;
|
|
732
941
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
942
|
+
// Credential readiness is process-local. With several runtimes serving one
|
|
943
|
+
// agent, writing it straight onto the shared row lets the last reporter win
|
|
944
|
+
// — so the report is stored on its own runtime and the logical state is
|
|
945
|
+
// recomputed from all live runtimes.
|
|
946
|
+
const runtimeInstanceId = singleHeaderValue(req, "x-runtime-instance-id");
|
|
947
|
+
const multiRuntime = isMultiRuntimeEnabled();
|
|
948
|
+
// Only the readiness write is process-scoped; model/status metadata on this
|
|
949
|
+
// same endpoint stays agent-level and needs no runtime identity.
|
|
950
|
+
if (multiRuntime && parsed.body.ready !== undefined && !runtimeInstanceId) {
|
|
951
|
+
jsonError(res, "X-Runtime-Instance-ID is required when multi-runtime mode is enabled", 400);
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
let agent = existing;
|
|
956
|
+
if (parsed.body.ready !== undefined) {
|
|
957
|
+
if (multiRuntime && runtimeInstanceId) {
|
|
958
|
+
if (setRuntimeCredentialReady(runtimeInstanceId, parsed.params.id, parsed.body.ready)) {
|
|
959
|
+
updateAgentCredentialMissing(parsed.params.id, parsed.body.missing ?? null);
|
|
960
|
+
reconcileAgentStatusFromRuntimes(parsed.params.id);
|
|
961
|
+
}
|
|
962
|
+
agent = getAgentById(parsed.params.id) ?? existing;
|
|
963
|
+
} else {
|
|
964
|
+
agent =
|
|
965
|
+
updateAgentCredentialState(
|
|
736
966
|
parsed.params.id,
|
|
737
967
|
parsed.body.ready,
|
|
738
968
|
parsed.body.missing ?? null,
|
|
739
|
-
) ?? existing
|
|
740
|
-
|
|
969
|
+
) ?? existing;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
741
972
|
if (!agent) {
|
|
742
973
|
jsonError(res, "Agent not found", 404);
|
|
743
974
|
return true;
|
package/src/http/all-routes.ts
CHANGED
package/src/http/config.ts
CHANGED
|
@@ -8,8 +8,12 @@ import {
|
|
|
8
8
|
getSwarmConfigLookupById,
|
|
9
9
|
getSwarmConfigs,
|
|
10
10
|
maskSecrets,
|
|
11
|
-
upsertSwarmConfig,
|
|
12
11
|
} from "../be/db";
|
|
12
|
+
import {
|
|
13
|
+
AGENT_MAX_TASKS_CONFIG_KEY,
|
|
14
|
+
resetAgentMaxTasksMirror,
|
|
15
|
+
upsertSwarmConfigWithPolicyMirror,
|
|
16
|
+
} from "../be/multi-runtime";
|
|
13
17
|
import { getUserGrant } from "../be/rbac-roles";
|
|
14
18
|
import {
|
|
15
19
|
isReservedConfigKey,
|
|
@@ -394,7 +398,7 @@ export async function handleConfig(
|
|
|
394
398
|
|
|
395
399
|
try {
|
|
396
400
|
const includeSecrets = queryParams.get("includeSecrets") === "true";
|
|
397
|
-
const config =
|
|
401
|
+
const config = upsertSwarmConfigWithPolicyMirror({
|
|
398
402
|
scope,
|
|
399
403
|
scopeId: scopeId || null,
|
|
400
404
|
key,
|
|
@@ -435,6 +439,13 @@ export async function handleConfig(
|
|
|
435
439
|
if (existing.scope === "global") {
|
|
436
440
|
scheduleIntegrationsReload();
|
|
437
441
|
}
|
|
442
|
+
if (
|
|
443
|
+
existing.scope === "agent" &&
|
|
444
|
+
existing.scopeId &&
|
|
445
|
+
existing.key === AGENT_MAX_TASKS_CONFIG_KEY
|
|
446
|
+
) {
|
|
447
|
+
resetAgentMaxTasksMirror(existing.scopeId);
|
|
448
|
+
}
|
|
438
449
|
deleteConfig.respond(res, 200, { success: true });
|
|
439
450
|
return true;
|
|
440
451
|
}
|
package/src/http/core.ts
CHANGED
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
shouldBlockPolling,
|
|
11
11
|
updateAgentStatus,
|
|
12
12
|
} from "../be/db";
|
|
13
|
+
import {
|
|
14
|
+
markRuntimeInstanceOffline,
|
|
15
|
+
reconcileAgentStatusFromRuntimes,
|
|
16
|
+
touchRuntimeInstance,
|
|
17
|
+
} from "../be/multi-runtime";
|
|
13
18
|
import { enqueueAdmissionRow } from "../be/rbac-audit";
|
|
14
19
|
import { getUserGrant } from "../be/rbac-roles";
|
|
15
20
|
import { initGitHub, resetGitHub } from "../github";
|
|
@@ -19,11 +24,12 @@ import { initLinear, resetLinear } from "../linear";
|
|
|
19
24
|
import { decideAdmission, isRbacEnabled } from "../rbac";
|
|
20
25
|
import { startSlackApp, stopSlackApp } from "../slack";
|
|
21
26
|
import type { AgentStatus } from "../types";
|
|
27
|
+
import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
|
|
22
28
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
23
29
|
import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
|
|
24
30
|
import { resolveHttpRequestAuth } from "./auth";
|
|
25
31
|
import { generateOpenApiSpec, SCALAR_HTML } from "./openapi";
|
|
26
|
-
import { findRoute, isPublicRoute } from "./route-def";
|
|
32
|
+
import { findRoute, isPublicRoute, route, runtimeInstanceHeader } from "./route-def";
|
|
27
33
|
import { agentWithCapacity, getPathSegments, jsonError, parseQueryParams } from "./utils";
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -273,6 +279,58 @@ export function _resetAutoReloadForTests(): void {
|
|
|
273
279
|
autoReloadInvocations = 0;
|
|
274
280
|
}
|
|
275
281
|
|
|
282
|
+
function singleHeader(req: IncomingMessage, name: string): string | undefined {
|
|
283
|
+
const raw = req.headers[name];
|
|
284
|
+
return Array.isArray(raw) ? raw[0] : raw;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const RUNTIME_HEADER_DOC =
|
|
288
|
+
"Workers may send `X-Runtime-Instance-ID`, the per-boot identifier of the calling process. " +
|
|
289
|
+
"It is ignored unless MULTI_RUNTIME_ENABLED is set.";
|
|
290
|
+
|
|
291
|
+
const pingRoute = route({
|
|
292
|
+
method: "post",
|
|
293
|
+
path: "/ping",
|
|
294
|
+
pattern: ["ping"],
|
|
295
|
+
summary: "Report agent liveness",
|
|
296
|
+
description:
|
|
297
|
+
`Refreshes the calling agent's status. ${RUNTIME_HEADER_DOC} With multi-runtime mode on, ` +
|
|
298
|
+
"the header must identify a live runtime of this agent; an absent, unknown, offline, or " +
|
|
299
|
+
"foreign identifier makes the call a no-op instead of an error, so workers predating the " +
|
|
300
|
+
"flag keep running.",
|
|
301
|
+
tags: ["Core"],
|
|
302
|
+
auth: { apiKey: true, agentId: true },
|
|
303
|
+
headers: runtimeInstanceHeader("refresh a runtime's liveness"),
|
|
304
|
+
rbac: { ungated: "self-scoped: an agent reports its own liveness" },
|
|
305
|
+
responses: {
|
|
306
|
+
204: { description: "Liveness recorded (or accepted as a no-op)" },
|
|
307
|
+
400: { description: "Missing X-Agent-ID header" },
|
|
308
|
+
404: { description: "Agent not found" },
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const closeRoute = route({
|
|
313
|
+
method: "post",
|
|
314
|
+
path: "/close",
|
|
315
|
+
pattern: ["close"],
|
|
316
|
+
summary: "Mark an agent or runtime offline on shutdown",
|
|
317
|
+
description:
|
|
318
|
+
`Retires the calling process. ${RUNTIME_HEADER_DOC} With multi-runtime mode on, the header ` +
|
|
319
|
+
"is required and only that runtime is retired; the agent goes offline once no live runtime " +
|
|
320
|
+
"remains. With the flag off, the agent is marked offline as before.",
|
|
321
|
+
tags: ["Core"],
|
|
322
|
+
auth: { apiKey: true, agentId: true },
|
|
323
|
+
headers: runtimeInstanceHeader("retire a runtime"),
|
|
324
|
+
rbac: { ungated: "self-scoped: an agent retires its own runtime" },
|
|
325
|
+
responses: {
|
|
326
|
+
204: { description: "Runtime (and agent, when last) marked offline" },
|
|
327
|
+
400: {
|
|
328
|
+
description: "Missing X-Agent-ID, or missing X-Runtime-Instance-ID in multi-runtime mode",
|
|
329
|
+
},
|
|
330
|
+
404: { description: "Agent not found" },
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
276
334
|
export async function handleCore(
|
|
277
335
|
req: IncomingMessage,
|
|
278
336
|
res: ServerResponse,
|
|
@@ -473,13 +531,16 @@ export async function handleCore(
|
|
|
473
531
|
return true;
|
|
474
532
|
}
|
|
475
533
|
|
|
476
|
-
if (req.method
|
|
534
|
+
if (pingRoute.match(req.method, getPathSegments(req.url || ""))) {
|
|
477
535
|
if (!myAgentId) {
|
|
478
536
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
479
537
|
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
480
538
|
return true;
|
|
481
539
|
}
|
|
482
540
|
|
|
541
|
+
const runtimeInstanceId = singleHeader(req, "x-runtime-instance-id");
|
|
542
|
+
const multiRuntime = isMultiRuntimeEnabled();
|
|
543
|
+
|
|
483
544
|
const tx = getDb().transaction(() => {
|
|
484
545
|
const agent = getAgentById(myAgentId);
|
|
485
546
|
|
|
@@ -489,6 +550,18 @@ export async function handleCore(
|
|
|
489
550
|
return false;
|
|
490
551
|
}
|
|
491
552
|
|
|
553
|
+
if (multiRuntime) {
|
|
554
|
+
// Prove the identity before touching the agent: the status ladder
|
|
555
|
+
// below resolves `offline` to `idle`, so an anonymous, unknown, or
|
|
556
|
+
// already-closed runtime could otherwise revive an agent whose
|
|
557
|
+
// runtimes have all exited. Failing that check is a no-op rather
|
|
558
|
+
// than an error — ping is non-destructive, and rejecting it would
|
|
559
|
+
// break workers that predate the flag.
|
|
560
|
+
if (!runtimeInstanceId || !touchRuntimeInstance(runtimeInstanceId, agent.id)) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
492
565
|
let status: AgentStatus = "idle";
|
|
493
566
|
|
|
494
567
|
if (agent.status === "busy") {
|
|
@@ -514,13 +587,29 @@ export async function handleCore(
|
|
|
514
587
|
return true;
|
|
515
588
|
}
|
|
516
589
|
|
|
517
|
-
if (req.method
|
|
590
|
+
if (closeRoute.match(req.method, getPathSegments(req.url || ""))) {
|
|
518
591
|
if (!myAgentId) {
|
|
519
592
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
520
593
|
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
521
594
|
return true;
|
|
522
595
|
}
|
|
523
596
|
|
|
597
|
+
const runtimeInstanceId = singleHeader(req, "x-runtime-instance-id");
|
|
598
|
+
const multiRuntime = isMultiRuntimeEnabled();
|
|
599
|
+
|
|
600
|
+
// Unlike ping, close is destructive, so it fails closed: an anonymous
|
|
601
|
+
// close would fall through to the legacy agent-wide close and take down
|
|
602
|
+
// an agent a sibling runtime is still serving.
|
|
603
|
+
if (multiRuntime && !runtimeInstanceId) {
|
|
604
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
605
|
+
res.end(
|
|
606
|
+
JSON.stringify({
|
|
607
|
+
error: "X-Runtime-Instance-ID is required when multi-runtime mode is enabled",
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
|
|
524
613
|
const tx = getDb().transaction(() => {
|
|
525
614
|
const agent = getAgentById(myAgentId);
|
|
526
615
|
|
|
@@ -530,7 +619,16 @@ export async function handleCore(
|
|
|
530
619
|
return false;
|
|
531
620
|
}
|
|
532
621
|
|
|
533
|
-
|
|
622
|
+
if (multiRuntime && runtimeInstanceId) {
|
|
623
|
+
// One process exiting retires only its own runtime; the logical state
|
|
624
|
+
// is then recomputed from whatever is left — offline when nothing is,
|
|
625
|
+
// otherwise reflecting the surviving runtimes' readiness and work.
|
|
626
|
+
markRuntimeInstanceOffline(runtimeInstanceId, agent.id);
|
|
627
|
+
reconcileAgentStatusFromRuntimes(agent.id);
|
|
628
|
+
} else {
|
|
629
|
+
// Legacy semantics, with or without a runtime header.
|
|
630
|
+
updateAgentStatus(agent.id, "offline");
|
|
631
|
+
}
|
|
534
632
|
|
|
535
633
|
return true;
|
|
536
634
|
});
|