@desplega.ai/agent-swarm 1.161.0 → 1.162.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 +3 -2
- package/dist/{acp-adapter-xx489vvb.js → acp-adapter-74m73ae9.js} +1 -1
- package/dist/{actions-ykjkn0m9.js → actions-4sr14gnt.js} +34 -31
- package/dist/{anthropic-messages-5qx80j9k.js → anthropic-messages-8xxrzky9.js} +22 -26
- package/dist/{app-0hdqa4k1.js → app-pt68fbwp.js} +13 -11
- package/dist/{artifact-3v5t9jtg.js → artifact-fey7dvj9.js} +3 -2
- package/dist/{assistant-am2g5g8a.js → assistant-cv3jytks.js} +37 -34
- package/dist/{azure-openai-responses-fszbkp0e.js → azure-openai-responses-f5cqsyj8.js} +15 -10
- package/dist/{boot-reembed-xy9t7avg.js → boot-reembed-4ht4gqth.js} +13 -11
- package/dist/{boot-reembed-gt5gjkt4.js → boot-reembed-nxpcvpj9.js} +12 -10
- package/dist/{boot-scrub-logs-9x0985y5.js → boot-scrub-logs-k93kyw8k.js} +9 -7
- package/dist/{boot-seeds-trv0d4vg.js → boot-seeds-1skcrs0e.js} +13 -10
- package/dist/{claude-adapter-jb6e1e54.js → claude-adapter-r5g0tzge.js} +23 -20
- package/dist/{claude-managed-setup-d1q9hv2b.js → claude-managed-setup-pkyrgshb.js} +3 -2
- package/dist/{claude-sdk-session-zga700s2.js → claude-sdk-session-jxyxnrwd.js} +5565 -5512
- package/dist/{cli-we0q7t0e.js → cli-2zy95vhm.js} +137 -64
- package/dist/{cli-35pj2yva.js → cli-4cwxn7gp.js} +38 -11
- package/dist/{cli-ad4qxm6j.js → cli-4dcpaefp.js} +2 -2
- package/dist/{cli-82c1pacn.js → cli-4x0csdjq.js} +71 -37
- package/dist/{cli-sqm3r5ps.js → cli-5bcd7sfy.js} +14 -18
- package/dist/{cli-d56tgd45.js → cli-5k6k8ta3.js} +2 -2
- package/dist/{cli-1er0rhz8.js → cli-5xhpb3kv.js} +15 -15
- package/dist/{cli-yrg7c73k.js → cli-7af8yhav.js} +10 -183
- package/dist/{cli-r6fsxc76.js → cli-7tre70zp.js} +513 -961
- package/dist/{cli-bh7jed3k.js → cli-8ev9xqnx.js} +34 -18
- package/dist/cli-bsky8g7w.js +449 -0
- package/dist/{cli-hn04t9db.js → cli-cebmgwsc.js} +14 -10
- package/dist/{cli-cfkrztza.js → cli-cvatsam7.js} +2 -2
- package/dist/{cli-2658f6at.js → cli-cvk6yzw5.js} +6 -8
- package/dist/{cli-ptr8h7kp.js → cli-eg0b3p0k.js} +5 -218
- package/dist/{cli-zb6xbk61.js → cli-g739zqzt.js} +4 -4
- package/dist/{cli-dv6cdjs2.js → cli-gd4favx9.js} +28 -7
- package/dist/{cli-5p3x4zna.js → cli-gpqpmns7.js} +2 -2
- package/dist/{cli-5nbmpy8w.js → cli-hb5c2x5y.js} +1 -1
- package/dist/{cli-jpf7e1zq.js → cli-hmsw1gpy.js} +3 -3
- package/dist/{cli-1cz0c18q.js → cli-jf7wmv1r.js} +2521 -733
- package/dist/{cli-qwsnmjdd.js → cli-jxs2ygaw.js} +168 -61
- package/dist/cli-kdkwd3yr.js +615 -0
- package/dist/{cli-hgmnf38m.js → cli-kh04eqwe.js} +1 -1
- package/dist/{cli-0383bp7f.js → cli-kp42yek0.js} +425 -90
- package/dist/{cli-ybgv4kw2.js → cli-mbssg1xv.js} +9 -2
- package/dist/{cli-jsp14b6c.js → cli-mzfth0bp.js} +38 -1
- package/dist/{cli-8g6mc14k.js → cli-nq7yqw7b.js} +16 -5
- package/dist/{cli-ee8bc46j.js → cli-nt8hecg9.js} +33 -32
- package/dist/{cli-9s68w752.js → cli-p0r8sxj5.js} +1 -1
- package/dist/{cli-58e8yr0d.js → cli-pkd6s2wy.js} +4 -2
- package/dist/{cli-17xpf7cm.js → cli-q24re23n.js} +6 -4
- package/dist/cli-qjjvzwzd.js +222 -0
- package/dist/cli-qrgt5bqf.js +189 -0
- package/dist/{cli-et8m9xsv.js → cli-rfjw9jw2.js} +22 -10
- package/dist/{cli-798vkbks.js → cli-rj0gqwwp.js} +2 -2
- package/dist/{cli-121ph74b.js → cli-rnyzn44p.js} +1 -1
- package/dist/{cli-vnnz5ncg.js → cli-s3hwpt9v.js} +95 -16
- package/dist/{cli-7119ja3w.js → cli-sptrj5f5.js} +2 -2
- package/dist/{cli-a8c82p2t.js → cli-sqvfa090.js} +13 -6
- package/dist/{cli-phfg8vy8.js → cli-swfmp1fx.js} +26 -26
- package/dist/{cli-nsdxtcbg.js → cli-tcen9p5w.js} +8 -5
- package/dist/{cli-9m03pzga.js → cli-vc9bp061.js} +5 -5
- package/dist/{cli-rtx9dygp.js → cli-vxe6qa4h.js} +1 -1
- package/dist/{cli-ecfxwnyb.js → cli-vy3ed88x.js} +1 -1
- package/dist/{cli-0c7ata9h.js → cli-vyqs9ya8.js} +6 -6
- package/dist/cli-wbvnn6tk.js +861 -0
- package/dist/{cli-kgd3p0bm.js → cli-wjgs5wxe.js} +6 -6
- package/dist/cli-wxkkd0e0.js +33 -0
- package/dist/{cli-akfj32vj.js → cli-xqpk3rp7.js} +5 -4
- package/dist/cli-zp38xvtm.js +9487 -0
- package/dist/{cli-3zc55ctx.js → cli-zvhd3f64.js} +2 -2
- package/dist/{cli-ze1d3wzv.js → cli-zxqx25nz.js} +5 -5
- package/dist/cli.js +32 -25
- package/dist/{cloudflare-workers-ai-system-one-k6fbxw9d.js → cloudflare-workers-ai-system-one-k8ddqjkq.js} +8 -6
- package/dist/{codex-adapter-04v97vep.js → codex-adapter-esvmnhn7.js} +13 -13
- package/dist/{codex-hook-20me6byw.js → codex-hook-m73jmqgx.js} +19 -3
- package/dist/{codex-session-runner-ax69mqq3.js → codex-session-runner-ban9excr.js} +13 -13
- package/dist/{commands-18gy08n1.js → commands-75413p2n.js} +9 -7
- package/dist/{db-d2pvjpyt.js → db-7f5pecwg.js} +11 -7
- package/dist/{dispatcher-8wbwyzjg.js → dispatcher-fh9at8nz.js} +33 -30
- package/dist/{dsh-adapter-ysqk9snv.js → dsh-adapter-wpze65d4.js} +3 -3
- package/dist/{e2b-esmt1r16.js → e2b-gjgdjp91.js} +10 -9
- package/dist/{pi-logo-animation-e9a0t68z.js → easter-egg-3d-3w5c2n4c.js} +122 -68
- package/dist/{execute-cmagrbrz.js → execute-f50wg9nh.js} +29 -6
- package/dist/{google-generative-ai-k6n5nbpp.js → google-generative-ai-jxxpnegy.js} +2 -2
- package/dist/{google-vertex-44xxtcvp.js → google-vertex-x7q1angm.js} +5 -5
- package/dist/{handlers-6b69m5kp.js → handlers-w13787bb.js} +36 -33
- package/dist/{hook-srnkt0hb.js → hook-46g1jrvx.js} +28 -15
- package/dist/{hook-k3mydf2n.js → hook-4tpts2ry.js} +20 -17
- package/dist/{http-hrf80ca1.js → http-19pzbfrt.js} +964 -176
- package/dist/{index-9rxdvj6w.js → index-8fd4dm4h.js} +34 -31
- package/dist/{index-1asbaj62.js → index-dqmp15bq.js} +35 -32
- package/dist/{index-45evq4ee.js → index-qb4q1y3e.js} +33 -30
- package/dist/{index-k4nj18qs.js → index-s75wc4wd.js} +12 -10
- package/dist/{index-xmevk2b9.js → index-scptvdxg.js} +27 -25
- package/dist/{keepalive-sfdz6ksp.js → keepalive-xa5ts9cd.js} +11 -9
- package/dist/{lead-ej91e9hw.js → lead-qp4xkvjq.js} +45 -41
- package/dist/{maintenance-80dnd7ke.js → maintenance-rm5885hm.js} +13 -11
- package/dist/{mistral-conversations-pc3ntnxt.js → mistral-conversations-rw7fbt22.js} +4 -4
- package/dist/{node-a5e00qhp.js → node-8zkdq42h.js} +834 -833
- package/dist/{oauth-refresh-sweep-m1etk2mx.js → oauth-refresh-sweep-1csdfebf.js} +10 -8
- package/dist/{onboard-0q8sdfqc.js → onboard-r6b78cs4.js} +10 -10
- package/dist/{openai-codex-responses-n7n683p2.js → openai-codex-responses-efvrgrq3.js} +4 -4
- package/dist/{openai-completions-jpaj5g2b.js → openai-completions-bbbrrz4z.js} +12 -8
- package/dist/{openai-responses-9a3nme9j.js → openai-responses-9jjyrcm1.js} +15 -10
- package/dist/{opencode-adapter-4wcgmvgf.js → opencode-adapter-kb3g6pkf.js} +2 -2
- package/dist/{otel-impl-hnqm9mxq.js → otel-impl-0y9knpky.js} +5 -5
- package/dist/{pi-mono-adapter-d6sqm9wr.js → pi-mono-adapter-z6s1bme2.js} +100 -19
- package/dist/{pricing-refresh-y60z64pm.js → pricing-refresh-mth26tgp.js} +11 -9
- package/dist/{rbac-roles-8pa0754b.js → rbac-roles-kwntb23q.js} +13 -9
- package/dist/rbac-roles-vpk4ebm8.js +45 -0
- package/dist/{registry-v5wytg0s.js → registry-km6t1jd2.js} +1 -1
- package/dist/{render-v2-418s1vj7.js → render-v2-hcm7h00y.js} +13 -11
- package/dist/{runtime-3pknc168.js → runtime-vfjsfsc5.js} +4 -4
- package/dist/{setup-yghdkj4x.js → setup-hbyzd65h.js} +2 -2
- package/dist/{virtual-modules-jcceevt5.js → virtual-modules-9bv7ry91.js} +9 -8
- package/dist/{work-objects-j1sz20v9.js → work-objects-m09sxnhe.js} +36 -33
- package/dist/{worker-d4gcvkjt.js → worker-0h35rvk4.js} +45 -41
- package/openapi.json +491 -12
- package/package.json +13 -6
- package/src/be/api-drain.ts +184 -0
- package/src/be/db/tasks/read.ts +31 -6
- package/src/be/db/tasks/write.ts +5 -4
- package/src/be/db.ts +42 -18
- package/src/be/memory/link-resolver.ts +29 -3
- package/src/be/migrations/192_favorites_agent_fs_path.sql +36 -0
- package/src/be/onboarding.ts +7 -1
- package/src/be/seed-prompt-templates.ts +1 -0
- package/src/be/swarm-config-guard.ts +35 -16
- package/src/be/task-citations.ts +11 -5
- package/src/be/users.ts +3 -0
- package/src/cli.tsx +3 -2
- package/src/comb/agent-fs.ts +39 -0
- package/src/comb/config.ts +37 -0
- package/src/comb/markers.ts +37 -0
- package/src/comb/review-batch.ts +486 -0
- package/src/comb/templates.ts +88 -0
- package/src/commands/context-preamble.ts +4 -1
- package/src/commands/hook.ts +2 -1
- package/src/commands/runner.ts +262 -68
- package/src/commands/templates.ts +10 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/fs/agent-fs-provider.ts +106 -2
- package/src/heartbeat/heartbeat.ts +4 -0
- package/src/hooks/codex-hook.ts +25 -1
- package/src/hooks/hook.ts +13 -0
- package/src/hooks/pr-body-guard.ts +983 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/assets.ts +5 -17
- package/src/http/comb.ts +131 -0
- package/src/http/core.ts +2 -1
- package/src/http/fs.ts +9 -17
- package/src/http/index.ts +23 -9
- package/src/http/onboarding.ts +21 -2
- package/src/http/poll.ts +30 -3
- package/src/http/realtime.ts +62 -0
- package/src/http/request-principal.ts +22 -0
- package/src/http/status.ts +34 -2
- package/src/kv-reserved-namespaces.ts +10 -5
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +123 -0
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/realtime/auth.ts +63 -1
- package/src/realtime/tickets.ts +52 -0
- package/src/realtime/transport.ts +51 -22
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -1
- package/src/telemetry-context.ts +175 -0
- package/src/telemetry-contract/EVENTS.md +81 -0
- package/src/telemetry-contract/SOURCE.json +9 -0
- package/src/telemetry-contract/catalog.json +1335 -0
- package/src/telemetry-contract/index.ts +60 -0
- package/src/telemetry-contract/types.gen.ts +1971 -0
- package/src/telemetry-identity.ts +129 -0
- package/src/telemetry-snapshot.ts +211 -0
- package/src/telemetry-trigger.ts +125 -0
- package/src/telemetry-types.typetest.ts +40 -0
- package/src/telemetry.ts +271 -67
- package/src/tests/api-drain-e2e.test.ts +249 -0
- package/src/tests/api-drain-worker-e2e.test.ts +216 -0
- package/src/tests/api-drain.test.ts +351 -0
- package/src/tests/comb-config-guard.test.ts +38 -0
- package/src/tests/comb-links.test.ts +162 -0
- package/src/tests/comb-markers.test.ts +68 -0
- package/src/tests/comb-review-batch.test.ts +770 -0
- package/src/tests/comb-status.test.ts +152 -0
- package/src/tests/db-task-read-contract.test.ts +2 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/favorites-agent-fs-path.test.ts +226 -0
- package/src/tests/memory-link-resolver.test.ts +34 -0
- package/src/tests/onboarding.test.ts +108 -0
- package/src/tests/pi-mono-adapter.test.ts +265 -2
- package/src/tests/pr-body-leaks.test.ts +789 -0
- package/src/tests/preload.ts +8 -0
- package/src/tests/rbac-engine.test.ts +19 -0
- package/src/tests/realtime-room-auth.test.ts +104 -1
- package/src/tests/realtime-tickets.test.ts +74 -0
- package/src/tests/realtime-transport.test.ts +131 -4
- package/src/tests/runner-api-drain.test.ts +301 -0
- package/src/tests/runner-fallback-output.test.ts +61 -0
- package/src/tests/runner-fenced-json-output.test.ts +178 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/send-task-requested-by.test.ts +133 -15
- package/src/tests/task-lifecycle-telemetry.test.ts +8 -3
- package/src/tests/telemetry-context.test.ts +314 -0
- package/src/tests/telemetry-init.test.ts +51 -19
- package/src/tests/telemetry-snapshot.test.ts +229 -0
- package/src/tests/telemetry-trigger.test.ts +200 -0
- package/src/tests/template-registry-helpers.ts +1 -0
- package/src/tests/workflow-agent-task-retry.test.ts +363 -0
- package/src/tests/workflow-definition-telemetry.test.ts +4 -4
- package/src/tools/poll-task.ts +20 -1
- package/src/tools/send-task.ts +9 -1
- package/src/tools/task-action.ts +11 -0
- package/src/types.ts +2 -1
- package/src/utils/api-drain.ts +24 -0
- package/src/utils/constants.ts +57 -16
- package/src/utils/pr-body-leaks.ts +172 -0
- package/src/utils/task-attachment-links.ts +14 -8
- package/src/workflows/recovery.ts +26 -1
- package/src/workflows/resume.ts +31 -4
- package/src/workflows/task-step-routing.ts +75 -2
- package/tsconfig.json +3 -0
- package/dist/cli-tjf4cn8z.js +0 -9119
- package/dist/cli-tsk3nz8a.js +0 -582
- package/dist/{cli-t6v2ah3r.js → cli-twrh04ap.js} +3 -3
package/src/commands/runner.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from "../providers/index.ts";
|
|
36
36
|
import type { SteerDeliveryResult } from "../providers/types.ts";
|
|
37
37
|
import { initTelemetry, telemetry } from "../telemetry.ts";
|
|
38
|
+
import { mapTriggerSurface } from "../telemetry-context.ts";
|
|
38
39
|
import {
|
|
39
40
|
isTerminalTaskStatus,
|
|
40
41
|
type ModelTierOverrides,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
resolveTaskModelSelection,
|
|
46
47
|
type SteeringMessage,
|
|
47
48
|
} from "../types.ts";
|
|
49
|
+
import { isApiDrainingResponse } from "../utils/api-drain.ts";
|
|
48
50
|
import { getApiKey } from "../utils/api-key.ts";
|
|
49
51
|
import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
|
|
50
52
|
import { isCodexAuthFailureReason } from "../utils/codex-auth-failure.ts";
|
|
@@ -716,8 +718,12 @@ export function scheduleSteeringDispatch(
|
|
|
716
718
|
return true;
|
|
717
719
|
}
|
|
718
720
|
|
|
719
|
-
/**
|
|
720
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Ping the server to indicate activity and update status. Resolves to whether
|
|
723
|
+
* the API is draining, or undefined when it did not answer (down, or a 5xx
|
|
724
|
+
* from a proxy), so a gap in service never reads as "stopped draining".
|
|
725
|
+
*/
|
|
726
|
+
export async function pingServer(config: ApiConfig, _role: string): Promise<boolean | undefined> {
|
|
721
727
|
const headers: Record<string, string> = {
|
|
722
728
|
"X-Agent-ID": config.agentId,
|
|
723
729
|
};
|
|
@@ -729,12 +735,15 @@ async function pingServer(config: ApiConfig, _role: string): Promise<void> {
|
|
|
729
735
|
}
|
|
730
736
|
|
|
731
737
|
try {
|
|
732
|
-
await fetch(`${config.apiUrl}/ping`, {
|
|
738
|
+
const response = await fetch(`${config.apiUrl}/ping`, {
|
|
733
739
|
method: "POST",
|
|
734
740
|
headers,
|
|
735
741
|
});
|
|
742
|
+
if (response.status >= 500) return undefined;
|
|
743
|
+
return isApiDrainingResponse(response);
|
|
736
744
|
} catch {
|
|
737
745
|
// Silently fail - server might not be running
|
|
746
|
+
return undefined;
|
|
738
747
|
}
|
|
739
748
|
}
|
|
740
749
|
|
|
@@ -1577,7 +1586,7 @@ async function validateProviderOutputIfNeeded(
|
|
|
1577
1586
|
config: ApiConfig,
|
|
1578
1587
|
taskId: string,
|
|
1579
1588
|
providerOutput: string,
|
|
1580
|
-
): Promise<{ ok: true } | { ok: false; failReason: string }> {
|
|
1589
|
+
): Promise<{ ok: true; output?: string } | { ok: false; failReason: string }> {
|
|
1581
1590
|
const headers: Record<string, string> = {
|
|
1582
1591
|
"Content-Type": "application/json",
|
|
1583
1592
|
};
|
|
@@ -1598,29 +1607,65 @@ async function validateProviderOutputIfNeeded(
|
|
|
1598
1607
|
return { ok: true };
|
|
1599
1608
|
}
|
|
1600
1609
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1610
|
+
// The first candidate is the raw text, so a final message that is already
|
|
1611
|
+
// bare JSON keeps its exact bytes. Later candidates recover JSON that the
|
|
1612
|
+
// model wrapped in a ```json fence or a line of prose.
|
|
1613
|
+
let firstValidationErrors: string[] | undefined;
|
|
1614
|
+
for (const [index, candidate] of jsonOutputCandidates(providerOutput).entries()) {
|
|
1615
|
+
let parsed: unknown;
|
|
1616
|
+
try {
|
|
1617
|
+
parsed = JSON.parse(candidate);
|
|
1618
|
+
} catch {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const validationErrors = validateJsonSchema(taskData.outputSchema, parsed);
|
|
1622
|
+
if (validationErrors.length === 0) {
|
|
1623
|
+
return index === 0 ? { ok: true } : { ok: true, output: candidate };
|
|
1624
|
+
}
|
|
1625
|
+
firstValidationErrors ??= validationErrors;
|
|
1610
1626
|
}
|
|
1611
1627
|
|
|
1612
|
-
|
|
1613
|
-
if (validationErrors.length > 0) {
|
|
1628
|
+
if (firstValidationErrors) {
|
|
1614
1629
|
return {
|
|
1615
1630
|
ok: false,
|
|
1616
|
-
failReason: `Structured output did not match outputSchema: ${
|
|
1631
|
+
failReason: `Structured output did not match outputSchema: ${firstValidationErrors.join("; ")}`,
|
|
1617
1632
|
};
|
|
1618
1633
|
}
|
|
1634
|
+
return {
|
|
1635
|
+
ok: false,
|
|
1636
|
+
failReason:
|
|
1637
|
+
"Structured output required by outputSchema but provider output was not valid JSON",
|
|
1638
|
+
};
|
|
1619
1639
|
} catch {
|
|
1620
1640
|
return { ok: true };
|
|
1621
1641
|
}
|
|
1642
|
+
}
|
|
1622
1643
|
|
|
1623
|
-
|
|
1644
|
+
/**
|
|
1645
|
+
* Texts that may hold the JSON in an agent's final message, in the order to
|
|
1646
|
+
* try them: the whole message, each fenced code block from last to first, then
|
|
1647
|
+
* the span from the first `{` (or `[`) to the last `}` (or `]`). Agents often
|
|
1648
|
+
* answer a schema-bound task with "Here is the result:" plus a ```json block
|
|
1649
|
+
* instead of calling store-progress, and `JSON.parse` on the whole message
|
|
1650
|
+
* fails on that. Fences run last to first because an earlier fence is often an
|
|
1651
|
+
* example or a draft that also matches the schema, and the answer comes last.
|
|
1652
|
+
*/
|
|
1653
|
+
function jsonOutputCandidates(text: string): string[] {
|
|
1654
|
+
const candidates = [text];
|
|
1655
|
+
const fenced: string[] = [];
|
|
1656
|
+
for (const match of text.matchAll(/```[a-zA-Z]*[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```/g)) {
|
|
1657
|
+
if (match[1]?.trim()) fenced.push(match[1].trim());
|
|
1658
|
+
}
|
|
1659
|
+
candidates.push(...fenced.reverse());
|
|
1660
|
+
for (const [open, close] of [
|
|
1661
|
+
["{", "}"],
|
|
1662
|
+
["[", "]"],
|
|
1663
|
+
] as const) {
|
|
1664
|
+
const start = text.indexOf(open);
|
|
1665
|
+
const end = text.lastIndexOf(close);
|
|
1666
|
+
if (start >= 0 && end > start) candidates.push(text.slice(start, end + 1));
|
|
1667
|
+
}
|
|
1668
|
+
return [...new Set(candidates)];
|
|
1624
1669
|
}
|
|
1625
1670
|
|
|
1626
1671
|
export async function ensureTaskFinished(
|
|
@@ -1658,7 +1703,7 @@ export async function ensureTaskFinished(
|
|
|
1658
1703
|
// the no-providerOutput path and the providerOutput-failed-schema-validation
|
|
1659
1704
|
// path below, so a schema'd task ending in free-form prose falls through to
|
|
1660
1705
|
// the same extraction fallback instead of hard-failing.
|
|
1661
|
-
const applyFallback = (fallback: FallbackResult) => {
|
|
1706
|
+
const applyFallback = (fallback: FallbackResult, rejectedOutputReason?: string) => {
|
|
1662
1707
|
console.log(`[${role}] Task ${taskId.slice(0, 8)} fallback result: ${fallback.kind}`);
|
|
1663
1708
|
switch (fallback.kind) {
|
|
1664
1709
|
case "extracted":
|
|
@@ -1677,7 +1722,11 @@ export async function ensureTaskFinished(
|
|
|
1677
1722
|
case "schema-fail":
|
|
1678
1723
|
status = "failed";
|
|
1679
1724
|
body.status = "failed";
|
|
1680
|
-
|
|
1725
|
+
// Keep why the provider's final message was rejected; the fallback's
|
|
1726
|
+
// own reason alone ("not provided via store-progress") hides it.
|
|
1727
|
+
body.failureReason = rejectedOutputReason
|
|
1728
|
+
? `${fallback.failReason}. Final message rejected: ${rejectedOutputReason}`
|
|
1729
|
+
: fallback.failReason;
|
|
1681
1730
|
break;
|
|
1682
1731
|
case "fetch-error":
|
|
1683
1732
|
body.output = `Process completed (could not verify task state: ${fallback.error})`;
|
|
@@ -1693,7 +1742,7 @@ export async function ensureTaskFinished(
|
|
|
1693
1742
|
} else if (providerOutput) {
|
|
1694
1743
|
const validation = await validateProviderOutputIfNeeded(config, taskId, providerOutput);
|
|
1695
1744
|
if (validation.ok) {
|
|
1696
|
-
body.output = providerOutput;
|
|
1745
|
+
body.output = validation.output ?? providerOutput;
|
|
1697
1746
|
} else {
|
|
1698
1747
|
// The task declared an outputSchema but the provider's final message
|
|
1699
1748
|
// is free-form prose that doesn't satisfy it. Don't convert this into
|
|
@@ -1707,7 +1756,7 @@ export async function ensureTaskFinished(
|
|
|
1707
1756
|
adapterType,
|
|
1708
1757
|
providerOutput,
|
|
1709
1758
|
);
|
|
1710
|
-
applyFallback(fallback);
|
|
1759
|
+
applyFallback(fallback, validation.failReason);
|
|
1711
1760
|
}
|
|
1712
1761
|
} else {
|
|
1713
1762
|
// Try structured output fallback if the task has an outputSchema
|
|
@@ -2310,6 +2359,8 @@ function setupShutdownHandlers(
|
|
|
2310
2359
|
`[${role}] Superseding ${state.activeTasks.size} remaining task(s) for resume after restart...`,
|
|
2311
2360
|
);
|
|
2312
2361
|
for (const [taskId, task] of state.activeTasks) {
|
|
2362
|
+
// Already superseded (or terminal) server-side, e.g. by the API-drain handoff.
|
|
2363
|
+
if (task.serverTerminalStatus) continue;
|
|
2313
2364
|
console.log(`[${role}] Superseding task ${taskId.slice(0, 8)}`);
|
|
2314
2365
|
task.session.abort("graceful_shutdown").catch(() => {});
|
|
2315
2366
|
if (apiConfig) {
|
|
@@ -2348,6 +2399,9 @@ function setupShutdownHandlers(
|
|
|
2348
2399
|
if (apiConfig) {
|
|
2349
2400
|
telemetry.session("ended", {
|
|
2350
2401
|
agentId: apiConfig.agentId,
|
|
2402
|
+
// A runner session spans many tasks, so it belongs to none.
|
|
2403
|
+
taskId: null,
|
|
2404
|
+
trigger_surface: null,
|
|
2351
2405
|
durationMs: state ? Date.now() - state.startedAt : undefined,
|
|
2352
2406
|
tasksProcessed: state?.tasksProcessed ?? 0,
|
|
2353
2407
|
});
|
|
@@ -2412,6 +2466,10 @@ export interface RunningTask {
|
|
|
2412
2466
|
promise: Promise<ProviderResult>;
|
|
2413
2467
|
/** The trigger type that caused this task to be spawned */
|
|
2414
2468
|
triggerType?: string;
|
|
2469
|
+
/** Surface that started the task's whole chain (root task source); tags session telemetry */
|
|
2470
|
+
triggerSurface?: string | null;
|
|
2471
|
+
/** `users.id` of the requester; hashed into `user_ref` on session telemetry */
|
|
2472
|
+
requestedByUserId?: string;
|
|
2415
2473
|
/** Set when the promise resolves, enabling non-blocking completion checks */
|
|
2416
2474
|
result: ProviderResult | null;
|
|
2417
2475
|
/** Deferred cursor updates for channel_activity triggers — committed after success */
|
|
@@ -2459,10 +2517,14 @@ export interface RunningTask {
|
|
|
2459
2517
|
abortRequestedAt?: number;
|
|
2460
2518
|
/**
|
|
2461
2519
|
* Terminal status the server already holds for this task, found by
|
|
2462
|
-
* `reconcileActiveTasks
|
|
2463
|
-
* the server's output and failureReason alone.
|
|
2520
|
+
* `reconcileActiveTasks` or set by an API-drain handoff. Completion then only
|
|
2521
|
+
* frees the slot and leaves the server's output and failureReason alone.
|
|
2464
2522
|
*/
|
|
2465
2523
|
serverTerminalStatus?: string;
|
|
2524
|
+
/** API-drain handoff calls made for this task (see `handOffTasksForApiDrain`). */
|
|
2525
|
+
drainHandoffAttempts?: number;
|
|
2526
|
+
/** The API answered the drain handoff for this task, so it is not retried. */
|
|
2527
|
+
drainHandoffSettled?: boolean;
|
|
2466
2528
|
}
|
|
2467
2529
|
|
|
2468
2530
|
/** Runner state for tracking concurrent tasks */
|
|
@@ -2494,6 +2556,12 @@ export interface RunnerState {
|
|
|
2494
2556
|
* value is the reset time in ms. Read by `selectCredential` (T5).
|
|
2495
2557
|
*/
|
|
2496
2558
|
modelWindowBlocks: Map<string, number>;
|
|
2559
|
+
/**
|
|
2560
|
+
* The API is draining (it sent `X-Swarm-Draining`): in-flight tasks are handed
|
|
2561
|
+
* off and no new work is taken until a response arrives without the header.
|
|
2562
|
+
* Unset against an API that never drains.
|
|
2563
|
+
*/
|
|
2564
|
+
apiDraining?: boolean;
|
|
2497
2565
|
}
|
|
2498
2566
|
|
|
2499
2567
|
/** Buffer for session logs */
|
|
@@ -2937,6 +3005,15 @@ async function triggerHeartbeatSweep(config: ApiConfig): Promise<boolean> {
|
|
|
2937
3005
|
}
|
|
2938
3006
|
}
|
|
2939
3007
|
|
|
3008
|
+
/**
|
|
3009
|
+
* `trigger_surface` for a worker event: the server-resolved root surface of the
|
|
3010
|
+
* task, or null when the server did not send one (an older server, or a
|
|
3011
|
+
* session with no task).
|
|
3012
|
+
*/
|
|
3013
|
+
function workerTriggerSurface(raw: string | null | undefined) {
|
|
3014
|
+
return raw ? mapTriggerSurface(raw) : null;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
2940
3017
|
/** Trigger types returned by the poll API */
|
|
2941
3018
|
interface Trigger {
|
|
2942
3019
|
type:
|
|
@@ -2947,6 +3024,8 @@ interface Trigger {
|
|
|
2947
3024
|
| "channel_activity"
|
|
2948
3025
|
| "budget_refused";
|
|
2949
3026
|
taskId?: string;
|
|
3027
|
+
/** Surface that started the task's whole chain (server-resolved); tags session telemetry. */
|
|
3028
|
+
triggerSurface?: string;
|
|
2950
3029
|
task?: unknown;
|
|
2951
3030
|
mentionsCount?: number;
|
|
2952
3031
|
count?: number;
|
|
@@ -2992,7 +3071,7 @@ interface Trigger {
|
|
|
2992
3071
|
}
|
|
2993
3072
|
|
|
2994
3073
|
/** Options for polling */
|
|
2995
|
-
interface PollOptions {
|
|
3074
|
+
export interface PollOptions {
|
|
2996
3075
|
apiUrl: string;
|
|
2997
3076
|
apiKey: string;
|
|
2998
3077
|
agentId: string;
|
|
@@ -3003,6 +3082,8 @@ interface PollOptions {
|
|
|
3003
3082
|
since?: string; // Optional: for filtering finished tasks
|
|
3004
3083
|
/** Live harness provider; keys the MODEL_TIER_* overrides sent with the poll. */
|
|
3005
3084
|
harnessProvider?: ProviderName;
|
|
3085
|
+
/** Told, on every answered poll, whether the API is draining. */
|
|
3086
|
+
onApiDrainSignal?: (draining: boolean) => void;
|
|
3006
3087
|
}
|
|
3007
3088
|
|
|
3008
3089
|
/**
|
|
@@ -3197,8 +3278,8 @@ export async function registerAgent(opts: {
|
|
|
3197
3278
|
return {};
|
|
3198
3279
|
}
|
|
3199
3280
|
|
|
3200
|
-
/** Poll for triggers via HTTP API */
|
|
3201
|
-
async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
|
|
3281
|
+
/** Poll for triggers via HTTP API. Exported for tests. */
|
|
3282
|
+
export async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
|
|
3202
3283
|
if (!isPollTracingEnabled()) {
|
|
3203
3284
|
return pollForTriggerOnce(opts);
|
|
3204
3285
|
}
|
|
@@ -3262,10 +3343,15 @@ async function pollForTriggerOnce(opts: PollOptions): Promise<Trigger | null> {
|
|
|
3262
3343
|
continue;
|
|
3263
3344
|
}
|
|
3264
3345
|
|
|
3346
|
+
const draining = isApiDrainingResponse(response);
|
|
3347
|
+
opts.onApiDrainSignal?.(draining);
|
|
3348
|
+
|
|
3265
3349
|
const data = (await response.json()) as { trigger: Trigger | null };
|
|
3266
3350
|
if (data.trigger) {
|
|
3267
3351
|
return data.trigger;
|
|
3268
3352
|
}
|
|
3353
|
+
// A draining API dispatches nothing: end the window so the loop can hand off.
|
|
3354
|
+
if (draining) return null;
|
|
3269
3355
|
} catch (error) {
|
|
3270
3356
|
console.warn(`[runner] Poll request error: ${error}`);
|
|
3271
3357
|
}
|
|
@@ -3722,6 +3808,10 @@ async function spawnProviderProcess(
|
|
|
3722
3808
|
runnerSessionId: string;
|
|
3723
3809
|
iteration: number;
|
|
3724
3810
|
taskId?: string;
|
|
3811
|
+
/** Surface that started the task chain, for session telemetry. */
|
|
3812
|
+
triggerSurface?: string | null;
|
|
3813
|
+
/** `users.id` of the requester; hashed into `user_ref` on session telemetry. */
|
|
3814
|
+
requestedByUserId?: string;
|
|
3725
3815
|
model?: string;
|
|
3726
3816
|
modelTier?: string;
|
|
3727
3817
|
/** Server claim-time resolution (task.resolvedModel); wins over the local one. */
|
|
@@ -4496,21 +4586,28 @@ async function spawnProviderProcess(
|
|
|
4496
4586
|
"gen_ai.usage.output_tokens": result.cost.outputTokens ?? 0,
|
|
4497
4587
|
"agentswarm.cost.total_usd": result.cost.totalCostUsd ?? 0,
|
|
4498
4588
|
});
|
|
4499
|
-
telemetry.session(
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4589
|
+
telemetry.session(
|
|
4590
|
+
"cost",
|
|
4591
|
+
{
|
|
4592
|
+
agentId: opts.agentId,
|
|
4593
|
+
// Lets cost be split by the surface that started the work.
|
|
4594
|
+
taskId: realTaskId ?? null,
|
|
4595
|
+
trigger_surface: workerTriggerSurface(opts.triggerSurface),
|
|
4596
|
+
model: result.cost.model,
|
|
4597
|
+
provider: result.cost.provider ?? opts.harnessProvider,
|
|
4598
|
+
inputTokens: result.cost.inputTokens ?? 0,
|
|
4599
|
+
outputTokens: result.cost.outputTokens ?? 0,
|
|
4600
|
+
cacheReadTokens: result.cost.cacheReadTokens,
|
|
4601
|
+
cacheWriteTokens: result.cost.cacheWriteTokens,
|
|
4602
|
+
reasoningOutputTokens: result.cost.reasoningOutputTokens,
|
|
4603
|
+
thinkingTokens: result.cost.thinkingTokens,
|
|
4604
|
+
totalCostUsd: result.cost.totalCostUsd,
|
|
4605
|
+
durationMs: result.cost.durationMs,
|
|
4606
|
+
numTurns: result.cost.numTurns,
|
|
4607
|
+
isError: result.cost.isError,
|
|
4608
|
+
},
|
|
4609
|
+
{ userId: opts.requestedByUserId ?? null },
|
|
4610
|
+
);
|
|
4514
4611
|
try {
|
|
4515
4612
|
await saveCostData(
|
|
4516
4613
|
{ ...result.cost, taskId: realTaskId, sessionId: opts.runnerSessionId },
|
|
@@ -4635,6 +4732,8 @@ async function spawnProviderProcess(
|
|
|
4635
4732
|
startTime: new Date(),
|
|
4636
4733
|
promise,
|
|
4637
4734
|
result: null,
|
|
4735
|
+
triggerSurface: opts.triggerSurface,
|
|
4736
|
+
requestedByUserId: opts.requestedByUserId,
|
|
4638
4737
|
credentialInfo,
|
|
4639
4738
|
// Snapshot the provider + local-env trait of the adapter this session is
|
|
4640
4739
|
// spawned on, so the session-end sync decision survives a live provider
|
|
@@ -4697,6 +4796,69 @@ function requestSessionAbort(task: RunningTask, reason: string, at: number): voi
|
|
|
4697
4796
|
.catch(() => {});
|
|
4698
4797
|
}
|
|
4699
4798
|
|
|
4799
|
+
/** Handoff calls per task while the API drains; a call the API answered is never repeated. */
|
|
4800
|
+
export const API_DRAIN_HANDOFF_MAX_ATTEMPTS = 3;
|
|
4801
|
+
|
|
4802
|
+
/**
|
|
4803
|
+
* Fold the API's drain signal (the `X-Swarm-Draining` header on a ping or poll
|
|
4804
|
+
* answer) into `state`. `undefined` means the API did not answer: keep the last
|
|
4805
|
+
* known state. Exported for tests.
|
|
4806
|
+
*/
|
|
4807
|
+
export function applyApiDrainSignal(
|
|
4808
|
+
state: RunnerState,
|
|
4809
|
+
role: string,
|
|
4810
|
+
draining: boolean | undefined,
|
|
4811
|
+
): void {
|
|
4812
|
+
if (draining === undefined) return;
|
|
4813
|
+
if (draining && !state.apiDraining) {
|
|
4814
|
+
console.log(`[${role}] API is draining: handing off in-flight tasks, taking no new work`);
|
|
4815
|
+
} else if (!draining && state.apiDraining) {
|
|
4816
|
+
console.log(`[${role}] API stopped draining: resuming normal polling`);
|
|
4817
|
+
}
|
|
4818
|
+
state.apiDraining = draining;
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
/**
|
|
4822
|
+
* The API is draining, so it still serves but will stop soon: supersede each
|
|
4823
|
+
* in-flight task now, while the call can land, instead of in the SIGTERM
|
|
4824
|
+
* handler, which compose runs after the API is gone. Same API call and reason
|
|
4825
|
+
* as the SIGTERM handoff, so the resume follow-up is identical.
|
|
4826
|
+
*
|
|
4827
|
+
* A task the API superseded is marked server-terminal and its session aborted;
|
|
4828
|
+
* completion then frees the slot without a finish call. A failed call leaves
|
|
4829
|
+
* the task running, up to `API_DRAIN_HANDOFF_MAX_ATTEMPTS`, and the SIGTERM
|
|
4830
|
+
* handler stays the fallback. Exported for tests.
|
|
4831
|
+
*/
|
|
4832
|
+
export async function handOffTasksForApiDrain(
|
|
4833
|
+
state: RunnerState,
|
|
4834
|
+
role: string,
|
|
4835
|
+
apiConfig: ApiConfig,
|
|
4836
|
+
): Promise<void> {
|
|
4837
|
+
for (const [taskId, task] of state.activeTasks) {
|
|
4838
|
+
// A settled session reports its own result through `checkCompletedProcesses`.
|
|
4839
|
+
if (task.result !== null || task.drainHandoffSettled) continue;
|
|
4840
|
+
const attempts = task.drainHandoffAttempts ?? 0;
|
|
4841
|
+
if (attempts >= API_DRAIN_HANDOFF_MAX_ATTEMPTS) continue;
|
|
4842
|
+
task.drainHandoffAttempts = attempts + 1;
|
|
4843
|
+
|
|
4844
|
+
const outcome = await supersedeTaskViaAPI(apiConfig, role, taskId, "graceful_shutdown");
|
|
4845
|
+
if (!outcome.ok) {
|
|
4846
|
+
console.warn(
|
|
4847
|
+
`[${role}] Drain handoff for task ${taskId.slice(0, 8)} failed (attempt ${attempts + 1}/${API_DRAIN_HANDOFF_MAX_ATTEMPTS}); the task keeps running`,
|
|
4848
|
+
);
|
|
4849
|
+
continue;
|
|
4850
|
+
}
|
|
4851
|
+
task.drainHandoffSettled = true;
|
|
4852
|
+
if (outcome.kind === "resumed" || outcome.kind === "workflow-failed") {
|
|
4853
|
+
task.serverTerminalStatus = outcome.kind === "resumed" ? "superseded" : "failed";
|
|
4854
|
+
console.log(`[${role}] Handed off task ${taskId.slice(0, 8)} ahead of the API stopping`);
|
|
4855
|
+
requestSessionAbort(task, "graceful_shutdown", Date.now());
|
|
4856
|
+
}
|
|
4857
|
+
// "alreadyFinished": the task ended on its own. "rejected": the API refused
|
|
4858
|
+
// the supersede on purpose. Either way the session follows its normal path.
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
|
|
4700
4862
|
/**
|
|
4701
4863
|
* Keep `state.activeTasks` in line with the server, so a session whose promise
|
|
4702
4864
|
* never settles cannot hold an execution slot forever:
|
|
@@ -4794,6 +4956,8 @@ export async function checkCompletedProcesses(
|
|
|
4794
4956
|
taskId: string;
|
|
4795
4957
|
result: ProviderResult;
|
|
4796
4958
|
triggerType?: string;
|
|
4959
|
+
triggerSurface?: string | null;
|
|
4960
|
+
requestedByUserId?: string;
|
|
4797
4961
|
cursorUpdates?: Array<{ channelId: string; ts: string }>;
|
|
4798
4962
|
workingDir?: string;
|
|
4799
4963
|
credentialInfo?: RunningTask["credentialInfo"];
|
|
@@ -4817,6 +4981,8 @@ export async function checkCompletedProcesses(
|
|
|
4817
4981
|
taskId,
|
|
4818
4982
|
result: task.result,
|
|
4819
4983
|
triggerType: task.triggerType,
|
|
4984
|
+
triggerSurface: task.triggerSurface,
|
|
4985
|
+
requestedByUserId: task.requestedByUserId,
|
|
4820
4986
|
cursorUpdates: task.cursorUpdates,
|
|
4821
4987
|
workingDir: task.workingDir,
|
|
4822
4988
|
credentialInfo: task.credentialInfo,
|
|
@@ -4836,6 +5002,8 @@ export async function checkCompletedProcesses(
|
|
|
4836
5002
|
for (const {
|
|
4837
5003
|
taskId,
|
|
4838
5004
|
result,
|
|
5005
|
+
triggerSurface,
|
|
5006
|
+
requestedByUserId,
|
|
4839
5007
|
cursorUpdates,
|
|
4840
5008
|
workingDir,
|
|
4841
5009
|
credentialInfo,
|
|
@@ -4951,31 +5119,44 @@ export async function checkCompletedProcesses(
|
|
|
4951
5119
|
),
|
|
4952
5120
|
);
|
|
4953
5121
|
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
typeof harnessVariantMeta?.version === "string" ||
|
|
4962
|
-
typeof harnessVariantMeta?.version === "number"
|
|
4963
|
-
? String(harnessVariantMeta.version)
|
|
4964
|
-
: undefined,
|
|
4965
|
-
exitCode: result.exitCode,
|
|
4966
|
-
isError: result.exitCode !== 0,
|
|
4967
|
-
durationMs,
|
|
4968
|
-
});
|
|
4969
|
-
const sessionTelemetryEvent = resolveSessionTelemetryEvent(result);
|
|
4970
|
-
if (sessionTelemetryEvent) {
|
|
4971
|
-
telemetry.session(sessionTelemetryEvent, {
|
|
5122
|
+
const sessionTriggerSurface = workerTriggerSurface(triggerSurface);
|
|
5123
|
+
const sessionActor = { userId: requestedByUserId ?? null };
|
|
5124
|
+
telemetry.taskEvent(
|
|
5125
|
+
"session_completed",
|
|
5126
|
+
{
|
|
5127
|
+
taskId,
|
|
5128
|
+
trigger_surface: sessionTriggerSurface,
|
|
4972
5129
|
agentId: apiConfig.agentId,
|
|
4973
|
-
errorCategory: normalizeSessionErrorCategory(result.errorCategory),
|
|
4974
5130
|
provider: result.cost?.provider ?? harnessProvider,
|
|
4975
|
-
model: result.cost?.model ?? model,
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5131
|
+
model: result.cost?.model ?? model ?? "unknown",
|
|
5132
|
+
harnessVariant,
|
|
5133
|
+
harnessVersion:
|
|
5134
|
+
typeof harnessVariantMeta?.version === "string" ||
|
|
5135
|
+
typeof harnessVariantMeta?.version === "number"
|
|
5136
|
+
? String(harnessVariantMeta.version)
|
|
5137
|
+
: undefined,
|
|
5138
|
+
exitCode: result.exitCode,
|
|
5139
|
+
isError: result.exitCode !== 0,
|
|
5140
|
+
durationMs,
|
|
5141
|
+
},
|
|
5142
|
+
sessionActor,
|
|
5143
|
+
);
|
|
5144
|
+
const sessionTelemetryEvent = resolveSessionTelemetryEvent(result);
|
|
5145
|
+
if (sessionTelemetryEvent) {
|
|
5146
|
+
telemetry.session(
|
|
5147
|
+
sessionTelemetryEvent,
|
|
5148
|
+
{
|
|
5149
|
+
agentId: apiConfig.agentId,
|
|
5150
|
+
taskId,
|
|
5151
|
+
trigger_surface: sessionTriggerSurface,
|
|
5152
|
+
errorCategory: normalizeSessionErrorCategory(result.errorCategory),
|
|
5153
|
+
provider: result.cost?.provider ?? harnessProvider,
|
|
5154
|
+
model: result.cost?.model ?? model ?? "unknown",
|
|
5155
|
+
durationMs: result.cost?.durationMs ?? durationMs,
|
|
5156
|
+
wasRateLimited: result.rateLimitResetAt != null,
|
|
5157
|
+
},
|
|
5158
|
+
sessionActor,
|
|
5159
|
+
);
|
|
4979
5160
|
}
|
|
4980
5161
|
state.tasksProcessed += 1;
|
|
4981
5162
|
|
|
@@ -5245,6 +5426,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
5245
5426
|
}
|
|
5246
5427
|
telemetry.session("started", {
|
|
5247
5428
|
agentId,
|
|
5429
|
+
// The boot session is not tied to a task.
|
|
5430
|
+
taskId: null,
|
|
5431
|
+
trigger_surface: null,
|
|
5248
5432
|
harnessProvider: bootProvider,
|
|
5249
5433
|
model: bootModel || undefined,
|
|
5250
5434
|
role,
|
|
@@ -6307,8 +6491,13 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
6307
6491
|
const taskReconcileIntervalMs = resolveTaskReconcileIntervalMs();
|
|
6308
6492
|
|
|
6309
6493
|
while (true) {
|
|
6310
|
-
// Ping server on each iteration to keep status updated
|
|
6311
|
-
|
|
6494
|
+
// Ping server on each iteration to keep status updated. The answer also
|
|
6495
|
+
// carries the API's drain signal, which reaches workers at capacity that
|
|
6496
|
+
// do not poll.
|
|
6497
|
+
applyApiDrainSignal(state, role, await pingServer(apiConfig, role));
|
|
6498
|
+
if (state.apiDraining) {
|
|
6499
|
+
await handOffTasksForApiDrain(state, role, apiConfig);
|
|
6500
|
+
}
|
|
6312
6501
|
|
|
6313
6502
|
// Check for completed processes first and ensure tasks are marked as finished
|
|
6314
6503
|
await checkCompletedProcesses(state, role, apiConfig, cancelledSignaled);
|
|
@@ -6412,8 +6601,10 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
6412
6601
|
}
|
|
6413
6602
|
}
|
|
6414
6603
|
|
|
6415
|
-
// Only poll if we have capacity
|
|
6416
|
-
if (state.
|
|
6604
|
+
// Only poll if we have capacity and the API is not draining
|
|
6605
|
+
if (state.apiDraining) {
|
|
6606
|
+
await Bun.sleep(1000);
|
|
6607
|
+
} else if (state.activeTasks.size < state.maxConcurrent) {
|
|
6417
6608
|
if (Date.now() - lastOrphanRecoveryAt > ORPHAN_RECOVERY_INTERVAL_MS) {
|
|
6418
6609
|
lastOrphanRecoveryAt = Date.now();
|
|
6419
6610
|
const recoveredOrphans = await recoverOrphanedInProgressTasks(apiConfig);
|
|
@@ -6439,6 +6630,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
6439
6630
|
runtimeInstanceId,
|
|
6440
6631
|
pollTimeout: effectiveTimeout,
|
|
6441
6632
|
harnessProvider: state.harnessProvider,
|
|
6633
|
+
onApiDrainSignal: (draining) => applyApiDrainSignal(state, role, draining),
|
|
6442
6634
|
});
|
|
6443
6635
|
|
|
6444
6636
|
if (trigger) {
|
|
@@ -6792,6 +6984,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
6792
6984
|
runnerSessionId: taskRunnerSessionId,
|
|
6793
6985
|
iteration,
|
|
6794
6986
|
taskId: trigger.taskId,
|
|
6987
|
+
triggerSurface: trigger.triggerSurface,
|
|
6988
|
+
requestedByUserId: trigger.requestedBy?.id,
|
|
6795
6989
|
model: taskModel,
|
|
6796
6990
|
modelTier: taskModelTier,
|
|
6797
6991
|
resolvedModel: (trigger.task as { resolvedModel?: string } | undefined)
|
|
@@ -72,6 +72,16 @@ Audit event: {{event_id}}. {{recovery_guidance}}
|
|
|
72
72
|
category: "task_lifecycle",
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
// Sent at most once per session by the pi adapter when the model's final turn
|
|
76
|
+
// carries no text and no tool call (see PiMonoSession.repromptAfterEmptyFinalTurn).
|
|
77
|
+
registerTemplate({
|
|
78
|
+
eventType: "task.nudge.empty_final_turn",
|
|
79
|
+
header: "",
|
|
80
|
+
defaultBody: `Your last turn returned no text and no tool call. Finish now: if the task needs a result, call store-progress with it.`,
|
|
81
|
+
variables: [],
|
|
82
|
+
category: "task_lifecycle",
|
|
83
|
+
});
|
|
84
|
+
|
|
75
85
|
registerTemplate({
|
|
76
86
|
eventType: "task.trigger.unread_mentions",
|
|
77
87
|
header: "",
|