experimental-a2 0.20.0 → 0.22.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/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/actor-shared-DFET5q16.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
- package/dist/ai-EEfKyc-z.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +214 -67
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +3 -3
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +86 -73
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
- package/dist/internal-CvsrprnU.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-CIeL33wj.js +12 -0
- package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +3 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
- package/dist/server-qgez0Xny.d.ts.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-D0MhBvg6.d.ts +532 -0
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +406 -225
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +1598 -587
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
- package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +527 -299
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
- package/dist/wire-CV-Z7wz-.js.map +1 -0
- package/docs/01-quickstart.mdx +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +13 -2
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +12 -11
- package/docs/agents/03-conversations.mdx +36 -25
- package/docs/agents/04-harnesses.mdx +14 -7
- package/docs/client/03-presence.mdx +18 -14
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +29 -24
- package/docs/reference/01-core.mdx +7 -6
- package/docs/reference/02-server.mdx +250 -191
- package/docs/reference/04-ai.mdx +33 -23
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +25 -19
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +8 -8
- package/docs/schedulers/02-vercel-queues.mdx +2 -2
- package/docs/schedulers/03-qstash.mdx +4 -4
- package/docs/server/01-consumers.mdx +418 -0
- package/docs/server/02-timers.mdx +31 -21
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +61 -51
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +24 -10
- package/docs/stores/03-redis-http.mdx +8 -6
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- package/docs/upgrades/01-overview.mdx +31 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +39 -7
- package/examples/playground/app/a0/chat.test.tsx +2 -2
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +209 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +12 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +5 -2
- package/sql/postgres/drizzle-schema.ts +209 -0
- package/sql/postgres/schema.sql +227 -67
- package/src/actor-shared.ts +10 -2
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +162 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +324 -82
- package/src/ai-harness.ts +17 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/client.ts +19 -2
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +27 -4
- package/src/errors.ts +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +57 -16
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +1 -1
- package/src/server.browser.ts +1 -1
- package/src/server.ts +819 -218
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +604 -286
- package/src/store-postgres.ts +2006 -857
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +1961 -1333
- package/src/store-redis-notify.ts +76 -26
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +915 -516
- package/src/store.ts +241 -102
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/actor-C7b-1arn.d.ts.map +0 -1
- package/dist/actor-shared-t7w8xIKP.js.map +0 -1
- package/dist/ai-harness-Clbg5ei7.js.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/client-BhHLoNKF.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/server-qyb5aJJG.js.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-presence-polling-B1ixeqIS.js +0 -94
- package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/server/01-handlers.mdx +0 -227
package/src/ai-fx.ts
CHANGED
|
@@ -34,18 +34,35 @@ import {
|
|
|
34
34
|
} from './ai-harness.ts'
|
|
35
35
|
import type { CompactionPartData, GenerationCompletedPayload } from './ai.ts'
|
|
36
36
|
import { schemaInput } from './ai-tool-input.ts'
|
|
37
|
+
import { HARNESS_PARKING } from './internal.ts'
|
|
37
38
|
import { boundedCache } from './bounded-cache.ts'
|
|
38
39
|
|
|
39
40
|
export type { Harness } from './ai-harness.ts'
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* The FX harness. Every model call FX makes is one durable step; A2 rebuilds
|
|
43
|
-
* the kernel for each step from its latest checkpoint and the log since
|
|
44
|
+
* the kernel for each step from its latest checkpoint and the log since,
|
|
45
|
+
* unless this process holds the kernel the step before left waiting on its
|
|
46
|
+
* tool calls.
|
|
44
47
|
*/
|
|
45
48
|
export function fx(): Harness<'fx', never> {
|
|
49
|
+
// Per agent, as its saved states are: sessions of two agents never share one.
|
|
50
|
+
const lots = new WeakMap<object, ParkingLot>()
|
|
51
|
+
const lotOf = (scope: object): ParkingLot => {
|
|
52
|
+
let lot = lots.get(scope)
|
|
53
|
+
if (lot === undefined) {
|
|
54
|
+
lot = parkingLot()
|
|
55
|
+
lots.set(scope, lot)
|
|
56
|
+
}
|
|
57
|
+
return lot
|
|
58
|
+
}
|
|
46
59
|
return defineHarness('fx', {
|
|
47
60
|
tools: {},
|
|
48
|
-
step: runStep,
|
|
61
|
+
step: (step) => runStep(step, lotOf(step.scope)),
|
|
62
|
+
release: ({ scope, sessionId }) => {
|
|
63
|
+
const entry = lots.get(scope)?.take(sessionId)
|
|
64
|
+
if (entry !== undefined) closeParked(entry)
|
|
65
|
+
},
|
|
49
66
|
})
|
|
50
67
|
}
|
|
51
68
|
|
|
@@ -72,6 +89,8 @@ type TurnPlan = {
|
|
|
72
89
|
/** Steering to queue while serving the call at that index. */
|
|
73
90
|
steers: Map<number, string[]>
|
|
74
91
|
end: 'stop' | 'interrupted' | 'live'
|
|
92
|
+
/** The attempt that made its last committed model call. */
|
|
93
|
+
generationId?: string
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
type Completion = { finishReason?: FinishReason; usage?: LanguageModelUsage }
|
|
@@ -92,6 +111,12 @@ type TurnState = {
|
|
|
92
111
|
parsed?: Promise<void>
|
|
93
112
|
}
|
|
94
113
|
|
|
114
|
+
/** A model request as the kernel sends it to the Gateway. */
|
|
115
|
+
type GatewayRequest = {
|
|
116
|
+
toolChoice?: { type?: string }
|
|
117
|
+
providerOptions?: Record<string, Record<string, unknown> | undefined>
|
|
118
|
+
}
|
|
119
|
+
|
|
95
120
|
function fail(state: TurnState, error: Error): never {
|
|
96
121
|
state.failure ??= error
|
|
97
122
|
state.fxTurn.cancel()
|
|
@@ -100,12 +125,102 @@ function fail(state: TurnState, error: Error): never {
|
|
|
100
125
|
|
|
101
126
|
const diverged = (message: string): Divergence => new Divergence('FX', message)
|
|
102
127
|
|
|
128
|
+
// ------------------------------------------------------------- kernels
|
|
129
|
+
|
|
130
|
+
/** A tool call the kernel waits on while A2's tool consumer runs it. */
|
|
131
|
+
type Waiting = {
|
|
132
|
+
toolName: string
|
|
133
|
+
input: unknown
|
|
134
|
+
resolve: (output: unknown) => void
|
|
135
|
+
reject: (error: unknown) => void
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** What a kernel's fetch and tools call: the step driving it now. */
|
|
139
|
+
type KernelHooks = {
|
|
140
|
+
fetch: (
|
|
141
|
+
input: string | URL | Request,
|
|
142
|
+
init?: RequestInit,
|
|
143
|
+
) => Promise<Response>
|
|
144
|
+
tool: (toolName: string, input: unknown) => unknown
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
type Kernel = { agent: FxAgent; hooks: KernelHooks; waiting: Waiting[] }
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A kernel a step left waiting on its tool calls. The session's next step in
|
|
151
|
+
* this process, in the same response or one that steered it, continues it
|
|
152
|
+
* with their results instead of rebuilding one from the log.
|
|
153
|
+
*/
|
|
154
|
+
type Parked = {
|
|
155
|
+
kernel: Kernel
|
|
156
|
+
fxTurn: FxTurn
|
|
157
|
+
drained: Promise<void>
|
|
158
|
+
/** The attempt whose model call the kernel made. */
|
|
159
|
+
generationId: string
|
|
160
|
+
settings: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
type ParkingLot = {
|
|
164
|
+
/** Removes the session's parked kernel; the caller then owns it. */
|
|
165
|
+
take(sessionId: string): Parked | undefined
|
|
166
|
+
park(sessionId: string, entry: Parked): void
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function closeParked(entry: Parked): void {
|
|
170
|
+
entry.fxTurn.cancel()
|
|
171
|
+
void entry.kernel.agent.close().catch(() => {})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* One agent's parked kernels: one per session, at most `maxKernels`. Each is
|
|
176
|
+
* closed when its session has nothing left to run in this process (the
|
|
177
|
+
* agent's release), when its turn ends, or when another takes its place.
|
|
178
|
+
*/
|
|
179
|
+
function parkingLot(): ParkingLot {
|
|
180
|
+
const held = new Map<string, Parked>()
|
|
181
|
+
const take = (sessionId: string): Parked | undefined => {
|
|
182
|
+
const entry = held.get(sessionId)
|
|
183
|
+
held.delete(sessionId)
|
|
184
|
+
return entry
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
take,
|
|
188
|
+
park(sessionId, entry) {
|
|
189
|
+
const replaced = take(sessionId)
|
|
190
|
+
if (replaced !== undefined) closeParked(replaced)
|
|
191
|
+
held.set(sessionId, entry)
|
|
192
|
+
// A turn that ends while it waits, as a cancelled one does, is done.
|
|
193
|
+
const ended = () => {
|
|
194
|
+
if (held.get(sessionId) !== entry) return
|
|
195
|
+
held.delete(sessionId)
|
|
196
|
+
closeParked(entry)
|
|
197
|
+
}
|
|
198
|
+
entry.fxTurn.result.then(ended, ended)
|
|
199
|
+
const oldest = held.keys().next().value
|
|
200
|
+
if (held.size > HARNESS_PARKING.maxKernels && oldest !== undefined) {
|
|
201
|
+
const evicted = take(oldest)
|
|
202
|
+
if (evicted !== undefined) closeParked(evicted)
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
103
208
|
// ---------------------------------------------------------------- step
|
|
104
209
|
|
|
105
|
-
async function runStep(
|
|
210
|
+
async function runStep(
|
|
211
|
+
step: HarnessStep,
|
|
212
|
+
lot: ParkingLot,
|
|
213
|
+
): Promise<HarnessGeneration> {
|
|
106
214
|
const { createFxAgent } = await loadLibfx()
|
|
107
215
|
const credential = gatewayCredential('fx')
|
|
108
216
|
const name = `${step.agentName}.a2.harness.fx`
|
|
217
|
+
const definitions = await toolDefinitions(step.tools)
|
|
218
|
+
// What the kernel is made with: one made otherwise can't continue the step.
|
|
219
|
+
const settings = JSON.stringify([
|
|
220
|
+
kernelModel(step),
|
|
221
|
+
step.instructions,
|
|
222
|
+
definitions,
|
|
223
|
+
])
|
|
109
224
|
const { replayAll, ...first } = await readHarness({
|
|
110
225
|
step,
|
|
111
226
|
name,
|
|
@@ -119,17 +234,25 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
119
234
|
// has read a response's finish, while A2 may still be reading its copy.
|
|
120
235
|
const released = new AbortController()
|
|
121
236
|
const network = AbortSignal.any([step.signal, released.signal])
|
|
122
|
-
let
|
|
237
|
+
let kernel: Kernel | undefined
|
|
238
|
+
/** Whether this step left its kernel for the next one. */
|
|
239
|
+
let parked = false
|
|
123
240
|
let turn: TurnState | undefined
|
|
124
241
|
|
|
125
242
|
const run = async (
|
|
126
243
|
controller: ReadableStreamDefaultController<UIMessageChunk>,
|
|
127
244
|
turns: TurnPlan[],
|
|
245
|
+
adopted: Parked | undefined,
|
|
128
246
|
): Promise<void> => {
|
|
129
247
|
controller.enqueue({ type: 'start', messageId: step.responseMessageId })
|
|
248
|
+
if (adopted !== undefined) {
|
|
249
|
+
await resume(controller, turns.at(-1)!, adopted)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
130
252
|
for (const plan of turns) {
|
|
131
|
-
if (stopped) return
|
|
132
|
-
|
|
253
|
+
if (stopped || step.signal.aborted) return
|
|
254
|
+
// The step's abort cancels the turn; the turn outlives it when parked.
|
|
255
|
+
const fxTurn = kernel!.agent.prompt(plan.prompt)
|
|
133
256
|
const state: TurnState = {
|
|
134
257
|
plan,
|
|
135
258
|
fxTurn,
|
|
@@ -161,37 +284,123 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
161
284
|
)
|
|
162
285
|
continue
|
|
163
286
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
287
|
+
await finishLive(controller, state, drained)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Continues a parked kernel: the results it waits on are in the log. */
|
|
292
|
+
const resume = async (
|
|
293
|
+
controller: ReadableStreamDefaultController<UIMessageChunk>,
|
|
294
|
+
plan: TurnPlan,
|
|
295
|
+
adopted: Parked,
|
|
296
|
+
): Promise<void> => {
|
|
297
|
+
const state: TurnState = {
|
|
298
|
+
plan,
|
|
299
|
+
fxTurn: adopted.fxTurn,
|
|
300
|
+
next: plan.calls.length,
|
|
301
|
+
served: plan.calls.at(-1)!.toolCalls,
|
|
302
|
+
answered: new Set(),
|
|
303
|
+
live: false,
|
|
304
|
+
liveStep: false,
|
|
305
|
+
toolsRequested: deferred(),
|
|
306
|
+
}
|
|
307
|
+
turn = state
|
|
308
|
+
// Steering goes in before any tool result, so FX's next model call can't
|
|
309
|
+
// start without it, as replay queues it before the recorded call ends.
|
|
310
|
+
for (const text of plan.steers.get(plan.calls.length - 1) ?? [])
|
|
311
|
+
await adopted.fxTurn.steer(text)
|
|
312
|
+
for (const waiting of kernel!.waiting.splice(0)) {
|
|
313
|
+
try {
|
|
314
|
+
const answered = answer(state, waiting.toolName, waiting.input)
|
|
315
|
+
if (answered === undefined)
|
|
316
|
+
fail(state, diverged(`no recorded result for ${waiting.toolName}`))
|
|
317
|
+
waiting.resolve(answered.output)
|
|
318
|
+
} catch (error) {
|
|
319
|
+
waiting.reject(error)
|
|
180
320
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
321
|
+
}
|
|
322
|
+
await finishLive(controller, state, adopted.drained)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const finishLive = async (
|
|
326
|
+
controller: ReadableStreamDefaultController<UIMessageChunk>,
|
|
327
|
+
state: TurnState,
|
|
328
|
+
drained: Promise<void>,
|
|
329
|
+
): Promise<void> => {
|
|
330
|
+
const outcome = await Promise.race([
|
|
331
|
+
state.fxTurn.result.then((result) => ({ ended: true, result })),
|
|
332
|
+
state.toolsRequested.promise.then(() => ({ ended: false as const })),
|
|
333
|
+
]).catch((error: unknown) => {
|
|
334
|
+
throw state.failure ?? error
|
|
335
|
+
})
|
|
336
|
+
if (state.failure) throw state.failure
|
|
337
|
+
await state.parsed
|
|
338
|
+
if (outcome.ended) {
|
|
339
|
+
if (
|
|
340
|
+
outcome.result.stopReason === 'cancelled' &&
|
|
341
|
+
!stopped &&
|
|
342
|
+
!step.signal.aborted
|
|
343
|
+
)
|
|
344
|
+
throw diverged('the turn was cancelled while this step ran it')
|
|
345
|
+
await drained
|
|
346
|
+
// Only a turn this attempt finished describes the log.
|
|
347
|
+
if (outcome.result.stopReason === 'end_turn' && !step.signal.aborted)
|
|
348
|
+
await saveCheckpoint()
|
|
349
|
+
} else if (kernel!.waiting.length > 0 && !stopped && !closed) {
|
|
350
|
+
// FX now waits on tools that A2's tool consumer runs; the next step in
|
|
351
|
+
// this process can answer it with their results.
|
|
352
|
+
lot.park(step.sessionId, {
|
|
353
|
+
kernel: kernel!,
|
|
354
|
+
fxTurn: state.fxTurn,
|
|
355
|
+
drained,
|
|
356
|
+
generationId: step.generationId,
|
|
357
|
+
settings,
|
|
187
358
|
})
|
|
359
|
+
parked = true
|
|
360
|
+
} else {
|
|
361
|
+
state.fxTurn.cancel()
|
|
188
362
|
}
|
|
363
|
+
controller.enqueue({ type: 'finish-step' })
|
|
364
|
+
controller.enqueue({
|
|
365
|
+
type: 'finish',
|
|
366
|
+
...(completion.finishReason === undefined
|
|
367
|
+
? {}
|
|
368
|
+
: { finishReason: completion.finishReason }),
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Whether a parked kernel stands where this step's log does: made the same
|
|
374
|
+
* way, its model call the last one the log's live turn committed, and a
|
|
375
|
+
* recorded result for each tool call it waits on. A steered response
|
|
376
|
+
* continues the same turn, so its steps qualify too.
|
|
377
|
+
*/
|
|
378
|
+
const adoptable = (entry: Parked, turns: TurnPlan[]): boolean => {
|
|
379
|
+
const plan = turns.at(-1)
|
|
380
|
+
if (
|
|
381
|
+
entry.settings !== settings ||
|
|
382
|
+
plan?.end !== 'live' ||
|
|
383
|
+
plan.generationId !== entry.generationId
|
|
384
|
+
)
|
|
385
|
+
return false
|
|
386
|
+
const unanswered = [...(plan.calls.at(-1)?.toolCalls ?? [])]
|
|
387
|
+
return entry.kernel.waiting.every((waiting) => {
|
|
388
|
+
const at = unanswered.findIndex(
|
|
389
|
+
(call) =>
|
|
390
|
+
call.toolName === waiting.toolName &&
|
|
391
|
+
sameJson(call.input, waiting.input) &&
|
|
392
|
+
results.has(call.toolCallId),
|
|
393
|
+
)
|
|
394
|
+
if (at === -1) return false
|
|
395
|
+
unanswered.splice(at, 1)
|
|
396
|
+
return true
|
|
397
|
+
})
|
|
189
398
|
}
|
|
190
399
|
|
|
191
400
|
const saveCheckpoint = async (): Promise<void> => {
|
|
192
401
|
let checkpoint: Uint8Array
|
|
193
402
|
try {
|
|
194
|
-
checkpoint = await agent
|
|
403
|
+
checkpoint = await kernel!.agent.checkpoint()
|
|
195
404
|
} catch {
|
|
196
405
|
// Later steps replay from the previous snapshot.
|
|
197
406
|
return
|
|
@@ -218,7 +427,8 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
218
427
|
const state = turn!
|
|
219
428
|
if (state.failure) throw state.failure
|
|
220
429
|
const body = decodeBody(init.body)
|
|
221
|
-
const
|
|
430
|
+
const request = JSON.parse(body) as GatewayRequest
|
|
431
|
+
const compaction = request.toolChoice?.type === 'none'
|
|
222
432
|
const recorded = state.plan.calls[state.next]
|
|
223
433
|
if (recorded !== undefined) {
|
|
224
434
|
if ((recorded.kind === 'compaction') !== compaction)
|
|
@@ -245,7 +455,12 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
245
455
|
return pending(init.signal)
|
|
246
456
|
}
|
|
247
457
|
state.live = true
|
|
248
|
-
return live(
|
|
458
|
+
return live(
|
|
459
|
+
state,
|
|
460
|
+
url,
|
|
461
|
+
{ ...init, body: withGatewayCaching(body, request) },
|
|
462
|
+
compaction,
|
|
463
|
+
)
|
|
249
464
|
}
|
|
250
465
|
|
|
251
466
|
const affinity = gatewaySessionAffinity({
|
|
@@ -256,18 +471,13 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
256
471
|
const live = async (
|
|
257
472
|
state: TurnState,
|
|
258
473
|
url: string,
|
|
259
|
-
init: RequestInit
|
|
474
|
+
init: RequestInit,
|
|
260
475
|
compaction: boolean,
|
|
261
476
|
): Promise<Response> => {
|
|
262
477
|
const headers = withAuth(init.headers, credential)
|
|
263
478
|
// The kernel's own affinity is new with every rebuild; A2's lasts the session.
|
|
264
479
|
headers.set('x-session-affinity', affinity)
|
|
265
|
-
const response = await fetch(url, {
|
|
266
|
-
...init,
|
|
267
|
-
body: withGatewayCaching(init.body),
|
|
268
|
-
headers,
|
|
269
|
-
signal: network,
|
|
270
|
-
})
|
|
480
|
+
const response = await fetch(url, { ...init, headers, signal: network })
|
|
271
481
|
// Abandoned before its headers came: the kernel has moved on without it.
|
|
272
482
|
if (init.signal?.aborted) {
|
|
273
483
|
void response.body?.cancel()
|
|
@@ -316,9 +526,12 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
316
526
|
})
|
|
317
527
|
}
|
|
318
528
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
529
|
+
/** The recorded result of a call from the step the kernel was served. */
|
|
530
|
+
const answer = (
|
|
531
|
+
state: TurnState,
|
|
532
|
+
toolName: string,
|
|
533
|
+
toolInput: unknown,
|
|
534
|
+
): { output: unknown } | undefined => {
|
|
322
535
|
const call = state.served.find(
|
|
323
536
|
(candidate) =>
|
|
324
537
|
candidate.toolName === toolName &&
|
|
@@ -326,24 +539,33 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
326
539
|
sameJson(candidate.input, toolInput),
|
|
327
540
|
)
|
|
328
541
|
const result = call === undefined ? undefined : results.get(call.toolCallId)
|
|
329
|
-
if (call
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
542
|
+
if (call === undefined || result === undefined) return undefined
|
|
543
|
+
state.answered.add(call.toolCallId)
|
|
544
|
+
if (result.error !== undefined) throw new Error(result.error)
|
|
545
|
+
if (result.denied === true) throw new Error('The tool call was denied.')
|
|
546
|
+
return { output: result.output ?? null }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const hostTool = (toolName: string, toolInput: unknown): unknown => {
|
|
550
|
+
const state = turn!
|
|
551
|
+
if (state.failure) throw state.failure
|
|
552
|
+
const answered = answer(state, toolName, toolInput)
|
|
553
|
+
if (answered !== undefined) return answered.output
|
|
335
554
|
if (state.live) {
|
|
336
555
|
state.toolsRequested.resolve()
|
|
337
|
-
return
|
|
556
|
+
return new Promise((resolve, reject) => {
|
|
557
|
+
kernel!.waiting.push({ toolName, input: toolInput, resolve, reject })
|
|
558
|
+
})
|
|
338
559
|
}
|
|
339
560
|
if (state.plan.end === 'interrupted') {
|
|
340
561
|
state.fxTurn.cancel()
|
|
341
562
|
return pending(undefined)
|
|
342
563
|
}
|
|
343
564
|
return fail(state, diverged(`no recorded result for ${toolName}`))
|
|
344
|
-
}
|
|
565
|
+
}
|
|
345
566
|
|
|
346
|
-
const
|
|
567
|
+
const hooks: KernelHooks = { fetch: hostFetch, tool: hostTool }
|
|
568
|
+
const planTurns = (turns: HarnessTurn[]): Promise<TurnPlan[]> =>
|
|
347
569
|
Promise.all(turns.map((recorded) => turnPlan(recorded, step.signal)))
|
|
348
570
|
let results = read.results
|
|
349
571
|
let controller: ReadableStreamDefaultController<UIMessageChunk> | undefined
|
|
@@ -352,17 +574,26 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
352
574
|
if (!closed) controller!.enqueue(chunk)
|
|
353
575
|
}
|
|
354
576
|
|
|
355
|
-
const create = (
|
|
356
|
-
|
|
577
|
+
const create = async (
|
|
578
|
+
checkpoint: Uint8Array | undefined,
|
|
579
|
+
): Promise<Kernel> => {
|
|
580
|
+
const created = { hooks, waiting: [] as Waiting[] }
|
|
581
|
+
const agent = await createFxAgent({
|
|
357
582
|
apiKey: credential.token,
|
|
358
583
|
model: kernelModel(step),
|
|
359
584
|
...(step.instructions.length === 0
|
|
360
585
|
? {}
|
|
361
586
|
: { instructions: step.instructions }),
|
|
362
|
-
tools:
|
|
587
|
+
tools: definitions.map((definition): FxHostTool => ({
|
|
588
|
+
...definition,
|
|
589
|
+
execute: (toolInput: unknown) =>
|
|
590
|
+
created.hooks.tool(definition.name, toolInput),
|
|
591
|
+
})),
|
|
363
592
|
...(checkpoint === undefined ? {} : { checkpoint }),
|
|
364
|
-
fetch:
|
|
593
|
+
fetch: (input, init) => created.hooks.fetch(input, init),
|
|
365
594
|
})
|
|
595
|
+
return Object.assign(created, { agent })
|
|
596
|
+
}
|
|
366
597
|
|
|
367
598
|
const stream = new ReadableStream<UIMessageChunk>({
|
|
368
599
|
start(streamController) {
|
|
@@ -374,16 +605,33 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
374
605
|
step.signal.addEventListener('abort', abort, { once: true })
|
|
375
606
|
void (async () => {
|
|
376
607
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
608
|
+
let turns = await planTurns(read.turns)
|
|
609
|
+
// An aborted step leaves a parked kernel for a later attempt.
|
|
610
|
+
const candidate = step.signal.aborted
|
|
611
|
+
? undefined
|
|
612
|
+
: lot.take(step.sessionId)
|
|
613
|
+
const adopted =
|
|
614
|
+
candidate !== undefined && adoptable(candidate, turns)
|
|
615
|
+
? candidate
|
|
616
|
+
: undefined
|
|
617
|
+
if (candidate !== undefined && adopted === undefined)
|
|
618
|
+
closeParked(candidate)
|
|
619
|
+
if (adopted !== undefined) {
|
|
620
|
+
kernel = adopted.kernel
|
|
621
|
+
kernel.hooks = hooks
|
|
622
|
+
} else {
|
|
623
|
+
try {
|
|
624
|
+
kernel = await create(read.saved)
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (read.saved === undefined) throw error
|
|
627
|
+
// An unusable checkpoint: replay the whole log instead.
|
|
628
|
+
read = await replayAll()
|
|
629
|
+
results = read.results
|
|
630
|
+
kernel = await create(undefined)
|
|
631
|
+
turns = await planTurns(read.turns)
|
|
632
|
+
}
|
|
385
633
|
}
|
|
386
|
-
await run(streamController,
|
|
634
|
+
await run(streamController, turns, adopted)
|
|
387
635
|
closed = true
|
|
388
636
|
streamController.close()
|
|
389
637
|
} catch (error) {
|
|
@@ -394,7 +642,7 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
394
642
|
} finally {
|
|
395
643
|
released.abort()
|
|
396
644
|
step.signal.removeEventListener('abort', abort)
|
|
397
|
-
await agent
|
|
645
|
+
if (!parked) await kernel?.agent.close().catch(() => {})
|
|
398
646
|
}
|
|
399
647
|
})()
|
|
400
648
|
},
|
|
@@ -417,21 +665,21 @@ async function turnPlan(
|
|
|
417
665
|
): Promise<TurnPlan> {
|
|
418
666
|
const calls: RecordedCall[] = []
|
|
419
667
|
const steers = new Map<number, string[]>()
|
|
420
|
-
const steer = (at: number, inputs: HarnessInput[] | undefined) => {
|
|
421
|
-
if (inputs !== undefined) steers.set(Math.max(0, at), inputs.map(steerText))
|
|
422
|
-
}
|
|
423
|
-
steer(0, turn.generations.length === 0 ? turn.steers.get(0) : undefined)
|
|
424
668
|
turn.generations.forEach((generation, index) => {
|
|
425
669
|
calls.push(...recordedCalls(generation.chunks, generation.completed))
|
|
670
|
+
const inputs = turn.steers.get(index)
|
|
426
671
|
// A generation's step is its last call.
|
|
427
|
-
|
|
672
|
+
if (inputs !== undefined)
|
|
673
|
+
steers.set(calls.length - 1, inputs.map(steerText))
|
|
428
674
|
})
|
|
429
675
|
const images = await harnessImages({
|
|
430
676
|
harness: 'fx()',
|
|
431
677
|
input: turn.prompt,
|
|
432
678
|
signal,
|
|
433
679
|
})
|
|
680
|
+
const generationId = turn.generations.at(-1)?.completed.generationId
|
|
434
681
|
return {
|
|
682
|
+
...(generationId === undefined ? {} : { generationId }),
|
|
435
683
|
prompt:
|
|
436
684
|
images.length === 0
|
|
437
685
|
? turn.prompt.text
|
|
@@ -788,10 +1036,7 @@ async function loadLibfx(): Promise<typeof import('libfx')> {
|
|
|
788
1036
|
* asks for it; options the request already has take precedence. Every other
|
|
789
1037
|
* byte stays as the kernel wrote it, a tool input's spacing included.
|
|
790
1038
|
*/
|
|
791
|
-
function withGatewayCaching(body: string): string {
|
|
792
|
-
const request = JSON.parse(body) as {
|
|
793
|
-
providerOptions?: Record<string, Record<string, unknown> | undefined>
|
|
794
|
-
}
|
|
1039
|
+
function withGatewayCaching(body: string, request: GatewayRequest): string {
|
|
795
1040
|
const options = request.providerOptions
|
|
796
1041
|
if (options === undefined)
|
|
797
1042
|
return body.replace(
|
|
@@ -814,6 +1059,9 @@ function withAuth(
|
|
|
814
1059
|
): Headers {
|
|
815
1060
|
const result = new Headers(headers)
|
|
816
1061
|
result.set('ai-gateway-auth-method', credential.method)
|
|
1062
|
+
// A parked kernel keeps the credential it was made with; the step's is current.
|
|
1063
|
+
if (result.has('authorization'))
|
|
1064
|
+
result.set('authorization', `Bearer ${credential.token}`)
|
|
817
1065
|
return result
|
|
818
1066
|
}
|
|
819
1067
|
|
|
@@ -833,17 +1081,16 @@ function kernelModel(
|
|
|
833
1081
|
}
|
|
834
1082
|
}
|
|
835
1083
|
|
|
836
|
-
|
|
1084
|
+
/** The agent's tools as the kernel takes them, without their execution. */
|
|
1085
|
+
async function toolDefinitions(
|
|
837
1086
|
tools: ToolSet,
|
|
838
|
-
|
|
839
|
-
): Promise<FxHostTool[]> {
|
|
1087
|
+
): Promise<{ name: string; description: string; inputSchema: object }[]> {
|
|
840
1088
|
return Promise.all(
|
|
841
1089
|
Object.entries(tools).map(async ([toolName, tool]) => ({
|
|
842
1090
|
name: toolName,
|
|
843
1091
|
description:
|
|
844
1092
|
typeof tool.description === 'string' ? tool.description : toolName,
|
|
845
1093
|
inputSchema: (await asSchema(tool.inputSchema).jsonSchema) as object,
|
|
846
|
-
execute: (input: unknown) => execute(toolName, input),
|
|
847
1094
|
})),
|
|
848
1095
|
)
|
|
849
1096
|
}
|
|
@@ -891,11 +1138,6 @@ function savedCheckpoint(state: unknown): Uint8Array | undefined {
|
|
|
891
1138
|
return new Uint8Array(Buffer.from(checkpoint, 'base64'))
|
|
892
1139
|
}
|
|
893
1140
|
|
|
894
|
-
function isCompaction(body: string): boolean {
|
|
895
|
-
const request = JSON.parse(body) as { toolChoice?: { type?: string } }
|
|
896
|
-
return request.toolChoice?.type === 'none'
|
|
897
|
-
}
|
|
898
|
-
|
|
899
1141
|
function decodeBody(body: BodyInit | null | undefined): string {
|
|
900
1142
|
if (typeof body === 'string') return body
|
|
901
1143
|
if (body instanceof Uint8Array || body instanceof ArrayBuffer)
|
package/src/ai-harness.ts
CHANGED
|
@@ -69,6 +69,8 @@ export type HarnessLog = {
|
|
|
69
69
|
|
|
70
70
|
/** Everything one durable generation hands its harness. */
|
|
71
71
|
export type HarnessStep = {
|
|
72
|
+
/** The agent's own identity: what a harness keeps in memory is per scope. */
|
|
73
|
+
scope: object
|
|
72
74
|
agentName: string
|
|
73
75
|
sessionId: string
|
|
74
76
|
requestIndex: number
|
|
@@ -95,6 +97,11 @@ export type HarnessDriver = {
|
|
|
95
97
|
/** Tools the harness brings; A2 executes them like the agent's own. */
|
|
96
98
|
readonly tools: ToolSet
|
|
97
99
|
step(step: HarnessStep): Promise<HarnessGeneration>
|
|
100
|
+
/**
|
|
101
|
+
* The session has nothing left to run in this process. Lets go of anything
|
|
102
|
+
* kept in memory for its next step; it holds nothing the log needs.
|
|
103
|
+
*/
|
|
104
|
+
release?(session: { scope: object; sessionId: string }): void
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
// A global symbol, so harness values work across separately bundled copies.
|
|
@@ -323,6 +330,7 @@ export type HarnessInput = { text: string; files: FileUIPart[] }
|
|
|
323
330
|
|
|
324
331
|
/** One harness turn to rebuild: its prompt, completed generations, and end. */
|
|
325
332
|
export type HarnessTurn = {
|
|
333
|
+
/** With any steering that entered before the turn's first completed generation. */
|
|
326
334
|
prompt: HarnessInput
|
|
327
335
|
generations: RecordedGeneration[]
|
|
328
336
|
/** Steering that entered after the generation at that index. */
|
|
@@ -441,8 +449,15 @@ export function harnessTurns(
|
|
|
441
449
|
files: response.prompt.flatMap((prompt) => prompt.files),
|
|
442
450
|
}
|
|
443
451
|
let turn = turns.at(-1)
|
|
444
|
-
if (steered && turn
|
|
445
|
-
|
|
452
|
+
if (steered && turn?.generations.length === 0) {
|
|
453
|
+
// Nothing answered the prompt yet, so the steering joins it, as messages
|
|
454
|
+
// queued before a response join its prompt.
|
|
455
|
+
turn.prompt = {
|
|
456
|
+
text: `${turn.prompt.text}\n\n${input.text}`,
|
|
457
|
+
files: [...turn.prompt.files, ...input.files],
|
|
458
|
+
}
|
|
459
|
+
} else if (steered && turn !== undefined) {
|
|
460
|
+
const at = turn.generations.length - 1
|
|
446
461
|
turn.steers.set(at, [...(turn.steers.get(at) ?? []), input])
|
|
447
462
|
} else {
|
|
448
463
|
turn = { prompt: input, generations: [], steers: new Map(), end: 'live' }
|
package/src/ai-pi.ts
CHANGED
|
@@ -436,7 +436,7 @@ async function runStep(
|
|
|
436
436
|
const result = results.get(toolCallId)
|
|
437
437
|
if (result !== undefined) return answeredTool(result, shape)
|
|
438
438
|
if (state.plan.end === 'live' && state.liveStep) {
|
|
439
|
-
// Pi now waits on tools that A2's tool
|
|
439
|
+
// Pi now waits on tools that A2's tool consumer run.
|
|
440
440
|
endStep(state)
|
|
441
441
|
return pending(signal)
|
|
442
442
|
}
|
|
@@ -962,7 +962,7 @@ function builtinDefinition(
|
|
|
962
962
|
|
|
963
963
|
/**
|
|
964
964
|
* The tools Pi's session declares: Pi's own, and the agent's. Their results
|
|
965
|
-
* come from A2's log; A2's tool
|
|
965
|
+
* come from A2's log; A2's tool consumer ran them.
|
|
966
966
|
*/
|
|
967
967
|
async function sessionTools(
|
|
968
968
|
modules: PiModules,
|
package/src/ai-server.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* experimental-a2/ai/server — `agent()`: the experimental-a2/ai protocol,
|
|
3
|
-
* its model and tool
|
|
3
|
+
* its model and tool consumers, and A2's store, scheduler, and telemetry,
|
|
4
4
|
* assembled as one ordinary A2 server.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export { agent,
|
|
7
|
+
export { agent, runWithConsumerContext } from './ai-agent.ts'
|
|
8
8
|
export type {
|
|
9
9
|
AISDKHarness,
|
|
10
10
|
Agent,
|