@dynamicagents/core 0.9.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.
Files changed (168) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +522 -0
  3. package/dist/a2a/agent-stub.d.ts +60 -0
  4. package/dist/a2a/agent-stub.js +1 -0
  5. package/dist/a2a/caller-token.d.ts +44 -0
  6. package/dist/a2a/caller-token.js +61 -0
  7. package/dist/a2a/caller.d.ts +23 -0
  8. package/dist/a2a/caller.js +33 -0
  9. package/dist/a2a/card.d.ts +158 -0
  10. package/dist/a2a/card.js +163 -0
  11. package/dist/a2a/context.d.ts +19 -0
  12. package/dist/a2a/context.js +61 -0
  13. package/dist/a2a/deliver.d.ts +159 -0
  14. package/dist/a2a/deliver.js +185 -0
  15. package/dist/a2a/executor.d.ts +84 -0
  16. package/dist/a2a/executor.js +106 -0
  17. package/dist/a2a/index.d.ts +35 -0
  18. package/dist/a2a/index.js +34 -0
  19. package/dist/a2a/notify.d.ts +106 -0
  20. package/dist/a2a/notify.js +180 -0
  21. package/dist/a2a/parts.d.ts +45 -0
  22. package/dist/a2a/parts.js +94 -0
  23. package/dist/a2a/push.d.ts +70 -0
  24. package/dist/a2a/push.js +53 -0
  25. package/dist/a2a/self-origin.d.ts +91 -0
  26. package/dist/a2a/self-origin.js +114 -0
  27. package/dist/a2a/task-store.d.ts +44 -0
  28. package/dist/a2a/task-store.js +99 -0
  29. package/dist/a2a/task.d.ts +85 -0
  30. package/dist/a2a/task.js +15 -0
  31. package/dist/a2a/verify.d.ts +80 -0
  32. package/dist/a2a/verify.js +143 -0
  33. package/dist/agent/budget.d.ts +46 -0
  34. package/dist/agent/budget.js +42 -0
  35. package/dist/agent/control.d.ts +109 -0
  36. package/dist/agent/control.js +115 -0
  37. package/dist/agent/errors.d.ts +85 -0
  38. package/dist/agent/errors.js +64 -0
  39. package/dist/agent/final-reply.d.ts +49 -0
  40. package/dist/agent/final-reply.js +68 -0
  41. package/dist/agent/history.d.ts +97 -0
  42. package/dist/agent/history.js +133 -0
  43. package/dist/agent/index.d.ts +29 -0
  44. package/dist/agent/index.js +29 -0
  45. package/dist/agent/inference.d.ts +110 -0
  46. package/dist/agent/inference.js +120 -0
  47. package/dist/agent/model.d.ts +90 -0
  48. package/dist/agent/model.js +1 -0
  49. package/dist/agent/session.d.ts +100 -0
  50. package/dist/agent/session.js +82 -0
  51. package/dist/agent/workers-ai/index.d.ts +23 -0
  52. package/dist/agent/workers-ai/index.js +23 -0
  53. package/dist/agent/workers-ai/runtime.d.ts +42 -0
  54. package/dist/agent/workers-ai/runtime.js +63 -0
  55. package/dist/alarm/index.d.ts +77 -0
  56. package/dist/alarm/index.js +116 -0
  57. package/dist/config.d.ts +202 -0
  58. package/dist/config.js +135 -0
  59. package/dist/contract/index.d.ts +9 -0
  60. package/dist/contract/index.js +8 -0
  61. package/dist/contract/plugin.d.ts +324 -0
  62. package/dist/contract/plugin.js +114 -0
  63. package/dist/contract/recipe.d.ts +180 -0
  64. package/dist/contract/recipe.js +1 -0
  65. package/dist/contract/validation.d.ts +91 -0
  66. package/dist/contract/validation.js +84 -0
  67. package/dist/db/db.d.ts +147 -0
  68. package/dist/db/db.js +90 -0
  69. package/dist/db/index.d.ts +8 -0
  70. package/dist/db/index.js +8 -0
  71. package/dist/db/migrations/index.d.ts +20 -0
  72. package/dist/db/migrations/index.js +60 -0
  73. package/dist/db/models/subtasks.d.ts +100 -0
  74. package/dist/db/models/subtasks.js +241 -0
  75. package/dist/db/models/tasks.d.ts +118 -0
  76. package/dist/db/models/tasks.js +274 -0
  77. package/dist/db/schema.d.ts +468 -0
  78. package/dist/db/schema.js +88 -0
  79. package/dist/env.d.ts +53 -0
  80. package/dist/env.js +47 -0
  81. package/dist/host/agent.d.ts +305 -0
  82. package/dist/host/agent.js +400 -0
  83. package/dist/host/index.d.ts +20 -0
  84. package/dist/host/index.js +19 -0
  85. package/dist/host/plugin-host.d.ts +42 -0
  86. package/dist/host/plugin-host.js +1 -0
  87. package/dist/index.d.ts +19 -0
  88. package/dist/index.js +17 -0
  89. package/dist/job/index.d.ts +21 -0
  90. package/dist/job/index.js +21 -0
  91. package/dist/job/lifecycle.d.ts +176 -0
  92. package/dist/job/lifecycle.js +230 -0
  93. package/dist/job/state.d.ts +92 -0
  94. package/dist/job/state.js +40 -0
  95. package/dist/platform.d.ts +138 -0
  96. package/dist/platform.js +140 -0
  97. package/dist/round/agent.d.ts +271 -0
  98. package/dist/round/agent.js +678 -0
  99. package/dist/round/index.d.ts +25 -0
  100. package/dist/round/index.js +23 -0
  101. package/dist/round/policy.d.ts +98 -0
  102. package/dist/round/policy.js +1 -0
  103. package/dist/round/subagent.d.ts +87 -0
  104. package/dist/round/subagent.js +108 -0
  105. package/dist/round/turn.d.ts +249 -0
  106. package/dist/round/turn.js +564 -0
  107. package/dist/round/workflow.d.ts +147 -0
  108. package/dist/round/workflow.js +387 -0
  109. package/dist/runtime/index.d.ts +127 -0
  110. package/dist/runtime/index.js +186 -0
  111. package/dist/runtime/tool-families.d.ts +32 -0
  112. package/dist/runtime/tool-families.js +61 -0
  113. package/dist/subagent/fingerprint.d.ts +37 -0
  114. package/dist/subagent/fingerprint.js +92 -0
  115. package/dist/subagent/index.d.ts +169 -0
  116. package/dist/subagent/index.js +330 -0
  117. package/dist/subagent/prompt.d.ts +32 -0
  118. package/dist/subagent/prompt.js +37 -0
  119. package/dist/subagent/run.d.ts +157 -0
  120. package/dist/subagent/run.js +540 -0
  121. package/dist/subagent/workspace.d.ts +85 -0
  122. package/dist/subagent/workspace.js +127 -0
  123. package/dist/subtasks/catalog.d.ts +40 -0
  124. package/dist/subtasks/catalog.js +36 -0
  125. package/dist/subtasks/decomposition.d.ts +85 -0
  126. package/dist/subtasks/decomposition.js +156 -0
  127. package/dist/subtasks/delegate.d.ts +120 -0
  128. package/dist/subtasks/delegate.js +131 -0
  129. package/dist/subtasks/index.d.ts +9 -0
  130. package/dist/subtasks/index.js +9 -0
  131. package/dist/subtasks/subtask-types.d.ts +91 -0
  132. package/dist/subtasks/subtask-types.js +103 -0
  133. package/dist/subtasks/types.d.ts +295 -0
  134. package/dist/subtasks/types.js +15 -0
  135. package/dist/testing/auth.d.ts +34 -0
  136. package/dist/testing/auth.js +35 -0
  137. package/dist/testing/do.d.ts +29 -0
  138. package/dist/testing/do.js +25 -0
  139. package/dist/testing/fake-session.d.ts +26 -0
  140. package/dist/testing/fake-session.js +37 -0
  141. package/dist/testing/fixtures.d.ts +64 -0
  142. package/dist/testing/fixtures.js +104 -0
  143. package/dist/testing/harness.d.ts +97 -0
  144. package/dist/testing/harness.js +138 -0
  145. package/dist/testing/index.d.ts +31 -0
  146. package/dist/testing/index.js +35 -0
  147. package/dist/testing/mock-model.d.ts +77 -0
  148. package/dist/testing/mock-model.js +136 -0
  149. package/dist/testing/node.d.ts +56 -0
  150. package/dist/testing/node.js +56 -0
  151. package/dist/testing/vcr-global-setup.d.ts +12 -0
  152. package/dist/testing/vcr-global-setup.js +15 -0
  153. package/dist/testing/vcr-shared.d.ts +38 -0
  154. package/dist/testing/vcr-shared.js +33 -0
  155. package/dist/testing/vcr-spec.d.ts +25 -0
  156. package/dist/testing/vcr-spec.js +124 -0
  157. package/dist/testing/vcr-store.d.ts +86 -0
  158. package/dist/testing/vcr-store.js +191 -0
  159. package/dist/testing/vcr.d.ts +117 -0
  160. package/dist/testing/vcr.js +275 -0
  161. package/dist/worker/define-agent.d.ts +123 -0
  162. package/dist/worker/define-agent.js +20 -0
  163. package/dist/worker/index.d.ts +218 -0
  164. package/dist/worker/index.js +369 -0
  165. package/eslint-rules/index.js +31 -0
  166. package/eslint-rules/no-deprecated-object-properties.js +81 -0
  167. package/package.json +178 -0
  168. package/scripts/generate-keys.mjs +48 -0
@@ -0,0 +1,106 @@
1
+ import { type JWK } from "jose";
2
+ import { type Task } from "@a2a-js/sdk";
3
+ import type { PlainTask } from "./task.js";
4
+ /**
5
+ * Outbound push-notification (accept + notify) helpers — the "notify" half of the
6
+ * async A2A contract. The gatekeeper dispatches `SendMessage` with a
7
+ * `taskPushNotificationConfig` (webhook `url` + validation `token`), we accept
8
+ * immediately with a `submitted` Task, and later POST the terminal Task back to
9
+ * that webhook. This module builds the Task shapes and signs + sends that
10
+ * callback.
11
+ *
12
+ * The callback is authenticated exactly like the AgentCard: a short-lived EdDSA
13
+ * JWT signed by `A2A_SIGNING_KEY`, whose protected header `kid`+`jku` must equal
14
+ * the card's signing `kid`+`jku` (see {@link file://./card.ts} `signCard`) — the
15
+ * gatekeeper pinned those at registration (Trust-On-First-Use) and verifies the
16
+ * callback token against that same public JWKS. No shared secret crosses the
17
+ * boundary; only our public key is ever used to verify.
18
+ */
19
+ /**
20
+ * The two shared values on this hop — the signature algorithm and the header
21
+ * carrying the per-task validation `token` — come from
22
+ * `@dynamicagents/g2a-protocol`, the same package the gatekeeper reads them from.
23
+ *
24
+ * The header used to be declared here and again in slack-gatekeeper, each with a
25
+ * comment naming the other file. It is a slightly different case from the claim
26
+ * names: the value is `@a2a-js/sdk`'s own default for `tokenHeaderName`, not
27
+ * Dynamic Agents' choice, but the SDK never *exports* it — it exists only as an
28
+ * inline fallback — so neither side could import it and both wrote it down.
29
+ *
30
+ * Re-exported so `@dynamicagents/core/a2a` keeps being where an agent finds it.
31
+ */
32
+ export { NOTIFICATION_TOKEN_HEADER } from "@dynamicagents/g2a-protocol";
33
+ /**
34
+ * The `submitted` Task we return synchronously to accept a turn (A2A §7.2). The
35
+ * gatekeeper only requires a non-empty `id`; the actual reply follows later via the
36
+ * callback.
37
+ */
38
+ export declare function buildSubmittedTask(taskId: string, contextId: string): PlainTask;
39
+ /**
40
+ * The terminal `completed` Task for a turn the agent deliberately did not
41
+ * answer — a turn an `AgentPlugin.shouldHandleTurn` gate declined, or whatever
42
+ * else a host treats as "nothing to say". Same shape as
43
+ * {@link buildSubmittedTask}: **no `status.message` at all**.
44
+ *
45
+ * The callback is still POSTed. The gatekeeper's pending row has to resolve — we
46
+ * simply hand it nothing to post to Slack. There is no `messageId` because there
47
+ * is no message, so unlike {@link buildCompletedTask} nothing needs a stable id
48
+ * for the gatekeeper to dedupe on: a `notify`-step retry re-delivers no content and
49
+ * is idempotent by construction.
50
+ */
51
+ export declare function buildNoReplyCompletedTask(taskId: string, contextId: string): PlainTask;
52
+ /**
53
+ * A non-terminal `working` Task snapshot carrying an intermediate content message.
54
+ * Streamed live from the DO as the tool loop emits content before the final reply.
55
+ *
56
+ * `messageId` is derived from `${taskId}:${key}` — stable across re-runs (see
57
+ * {@link agentTextMessage}) so the gatekeeper dedupes correctly on workflow replay.
58
+ *
59
+ * `key` is a **semantic** string, not a step counter, and the distinction is
60
+ * load-bearing: an agent that runs several rounds per task emits progress from
61
+ * each, so a bare index makes round 0's third step and round 1's third step the
62
+ * same message to the gatekeeper — it dedupes the second away and the user watches
63
+ * a task go quiet. A caller that genuinely has one flat sequence can pass
64
+ * `String(i)`; one with rounds should key on both (`r1:step:3`), and milestones
65
+ * on what they are (`ack:1`).
66
+ */
67
+ export declare function buildWorkingTask(taskId: string, contextId: string, text: string, key: string): PlainTask;
68
+ /**
69
+ * The terminal `completed` Task POSTed to the gatekeeper callback. The `messageId` is
70
+ * deterministic (`${taskId}:final`, not a fresh UUID) because this is built in the
71
+ * workflow body, which re-runs on replay: a random id would change on a notify-step
72
+ * retry and the gatekeeper would dedupe the final message as a new one and double-post.
73
+ */
74
+ export declare function buildCompletedTask(taskId: string, contextId: string, reply: string): PlainTask;
75
+ /**
76
+ * The terminal `failed` Task POSTed to the gatekeeper callback — an unexpected,
77
+ * non-transient failure aborted the turn.
78
+ *
79
+ * A2A v1.0 gives a task no structured error (`TaskStatus` is only
80
+ * `{state, message, timestamp}`), so the state *is* the failure signal and `text`
81
+ * is the only place to explain. Keep that text user-safe: the gatekeeper renders it
82
+ * to a human, under its own "⚠️ *Agent …* (failed):" prefix.
83
+ *
84
+ * Shares the `${taskId}:final` messageId with {@link buildCompletedTask} by
85
+ * design: a Task terminates exactly once and the two states are mutually
86
+ * exclusive, so only one of them is ever built and posted — and a notify retry
87
+ * re-posts that same one under the same dedupe key.
88
+ */
89
+ export declare function buildFailedTask(taskId: string, contextId: string, text: string): PlainTask;
90
+ /**
91
+ * Sign the callback JWT the gatekeeper verifies against our pinned card key. The
92
+ * protected header mirrors the card signature (`kid`+`jku`); `aud` must equal the
93
+ * exact webhook URL the gatekeeper handed us in the `taskPushNotificationConfig`.
94
+ */
95
+ export declare function signCallbackJwt(privateJwk: JWK & {
96
+ kid: string;
97
+ }, opts: {
98
+ jku: string;
99
+ aud: string;
100
+ }): Promise<string>;
101
+ /**
102
+ * POST a Task snapshot to the gatekeeper's push-notification webhook, wrapped in the
103
+ * v1.0 `StreamResponse` envelope. Returns the raw `Response` so the caller (the
104
+ * workflow's `notify` step) can decide whether a non-2xx warrants a retry.
105
+ */
106
+ export declare function postNotification(url: string, token: string, jwt: string, task: Task): Promise<Response>;
@@ -0,0 +1,180 @@
1
+ import { SignJWT } from "jose";
2
+ import { TaskState } from "@a2a-js/sdk";
3
+ import { V1PushNotificationSerializer } from "@a2a-js/sdk/server";
4
+ import { A2A_JWS_ALG, NOTIFICATION_TOKEN_HEADER } from "@dynamicagents/g2a-protocol";
5
+ import { agentTextMessage } from "./parts.js";
6
+ /**
7
+ * Outbound push-notification (accept + notify) helpers — the "notify" half of the
8
+ * async A2A contract. The gatekeeper dispatches `SendMessage` with a
9
+ * `taskPushNotificationConfig` (webhook `url` + validation `token`), we accept
10
+ * immediately with a `submitted` Task, and later POST the terminal Task back to
11
+ * that webhook. This module builds the Task shapes and signs + sends that
12
+ * callback.
13
+ *
14
+ * The callback is authenticated exactly like the AgentCard: a short-lived EdDSA
15
+ * JWT signed by `A2A_SIGNING_KEY`, whose protected header `kid`+`jku` must equal
16
+ * the card's signing `kid`+`jku` (see {@link file://./card.ts} `signCard`) — the
17
+ * gatekeeper pinned those at registration (Trust-On-First-Use) and verifies the
18
+ * callback token against that same public JWKS. No shared secret crosses the
19
+ * boundary; only our public key is ever used to verify.
20
+ */
21
+ /**
22
+ * The two shared values on this hop — the signature algorithm and the header
23
+ * carrying the per-task validation `token` — come from
24
+ * `@dynamicagents/g2a-protocol`, the same package the gatekeeper reads them from.
25
+ *
26
+ * The header used to be declared here and again in slack-gatekeeper, each with a
27
+ * comment naming the other file. It is a slightly different case from the claim
28
+ * names: the value is `@a2a-js/sdk`'s own default for `tokenHeaderName`, not
29
+ * Dynamic Agents' choice, but the SDK never *exports* it — it exists only as an
30
+ * inline fallback — so neither side could import it and both wrote it down.
31
+ *
32
+ * Re-exported so `@dynamicagents/core/a2a` keeps being where an agent finds it.
33
+ */
34
+ export { NOTIFICATION_TOKEN_HEADER } from "@dynamicagents/g2a-protocol";
35
+ /**
36
+ * Callback-JWT lifetime. The gatekeeper enforces `maxTokenAge: 10m` with a 60s clock
37
+ * tolerance, so keep this comfortably under that.
38
+ */
39
+ const CALLBACK_TOKEN_TTL = "5m";
40
+ /**
41
+ * The SDK's canonical v1.0 push-notification body encoder: the `StreamResponse`
42
+ * envelope as protobuf-JSON, with content type `application/a2a+json`. v1.0
43
+ * moved push notifications onto the same envelope the streaming transports use
44
+ * (v0.3 POSTed a bare `Task`), so the encoding is the SDK's rather than ours —
45
+ * the gatekeeper decodes it with `StreamResponse.fromJSON`.
46
+ */
47
+ const PUSH_SERIALIZER = new V1PushNotificationSerializer();
48
+ /** A Task snapshot in `state` carrying no message — nothing to say, only a state change. */
49
+ function buildBareTask(taskId, contextId, state) {
50
+ return {
51
+ id: taskId,
52
+ contextId,
53
+ status: {
54
+ state,
55
+ message: undefined,
56
+ timestamp: new Date().toISOString()
57
+ },
58
+ artifacts: [],
59
+ history: [],
60
+ metadata: undefined
61
+ };
62
+ }
63
+ /**
64
+ * The `submitted` Task we return synchronously to accept a turn (A2A §7.2). The
65
+ * gatekeeper only requires a non-empty `id`; the actual reply follows later via the
66
+ * callback.
67
+ */
68
+ export function buildSubmittedTask(taskId, contextId) {
69
+ return buildBareTask(taskId, contextId, TaskState.TASK_STATE_SUBMITTED);
70
+ }
71
+ /**
72
+ * The terminal `completed` Task for a turn the agent deliberately did not
73
+ * answer — a turn an `AgentPlugin.shouldHandleTurn` gate declined, or whatever
74
+ * else a host treats as "nothing to say". Same shape as
75
+ * {@link buildSubmittedTask}: **no `status.message` at all**.
76
+ *
77
+ * The callback is still POSTed. The gatekeeper's pending row has to resolve — we
78
+ * simply hand it nothing to post to Slack. There is no `messageId` because there
79
+ * is no message, so unlike {@link buildCompletedTask} nothing needs a stable id
80
+ * for the gatekeeper to dedupe on: a `notify`-step retry re-delivers no content and
81
+ * is idempotent by construction.
82
+ */
83
+ export function buildNoReplyCompletedTask(taskId, contextId) {
84
+ return buildBareTask(taskId, contextId, TaskState.TASK_STATE_COMPLETED);
85
+ }
86
+ /**
87
+ * A Task snapshot POSTed to the gatekeeper callback in a given `state`, carrying one
88
+ * `agent` message. The gatekeeper reads the reply from `status.message.parts`, so
89
+ * the text lives there.
90
+ */
91
+ function buildTaskUpdate(taskId, contextId, state, text, messageId) {
92
+ const task = buildBareTask(taskId, contextId, state);
93
+ task.status.message = agentTextMessage({
94
+ messageId,
95
+ text,
96
+ contextId,
97
+ taskId
98
+ });
99
+ return task;
100
+ }
101
+ /**
102
+ * A non-terminal `working` Task snapshot carrying an intermediate content message.
103
+ * Streamed live from the DO as the tool loop emits content before the final reply.
104
+ *
105
+ * `messageId` is derived from `${taskId}:${key}` — stable across re-runs (see
106
+ * {@link agentTextMessage}) so the gatekeeper dedupes correctly on workflow replay.
107
+ *
108
+ * `key` is a **semantic** string, not a step counter, and the distinction is
109
+ * load-bearing: an agent that runs several rounds per task emits progress from
110
+ * each, so a bare index makes round 0's third step and round 1's third step the
111
+ * same message to the gatekeeper — it dedupes the second away and the user watches
112
+ * a task go quiet. A caller that genuinely has one flat sequence can pass
113
+ * `String(i)`; one with rounds should key on both (`r1:step:3`), and milestones
114
+ * on what they are (`ack:1`).
115
+ */
116
+ export function buildWorkingTask(taskId, contextId, text, key) {
117
+ return buildTaskUpdate(taskId, contextId, TaskState.TASK_STATE_WORKING, text, `${taskId}:${key}`);
118
+ }
119
+ /**
120
+ * The terminal `completed` Task POSTed to the gatekeeper callback. The `messageId` is
121
+ * deterministic (`${taskId}:final`, not a fresh UUID) because this is built in the
122
+ * workflow body, which re-runs on replay: a random id would change on a notify-step
123
+ * retry and the gatekeeper would dedupe the final message as a new one and double-post.
124
+ */
125
+ export function buildCompletedTask(taskId, contextId, reply) {
126
+ return buildTaskUpdate(taskId, contextId, TaskState.TASK_STATE_COMPLETED, reply, `${taskId}:final`);
127
+ }
128
+ /**
129
+ * The terminal `failed` Task POSTed to the gatekeeper callback — an unexpected,
130
+ * non-transient failure aborted the turn.
131
+ *
132
+ * A2A v1.0 gives a task no structured error (`TaskStatus` is only
133
+ * `{state, message, timestamp}`), so the state *is* the failure signal and `text`
134
+ * is the only place to explain. Keep that text user-safe: the gatekeeper renders it
135
+ * to a human, under its own "⚠️ *Agent …* (failed):" prefix.
136
+ *
137
+ * Shares the `${taskId}:final` messageId with {@link buildCompletedTask} by
138
+ * design: a Task terminates exactly once and the two states are mutually
139
+ * exclusive, so only one of them is ever built and posted — and a notify retry
140
+ * re-posts that same one under the same dedupe key.
141
+ */
142
+ export function buildFailedTask(taskId, contextId, text) {
143
+ return buildTaskUpdate(taskId, contextId, TaskState.TASK_STATE_FAILED, text, `${taskId}:final`);
144
+ }
145
+ /**
146
+ * Sign the callback JWT the gatekeeper verifies against our pinned card key. The
147
+ * protected header mirrors the card signature (`kid`+`jku`); `aud` must equal the
148
+ * exact webhook URL the gatekeeper handed us in the `taskPushNotificationConfig`.
149
+ */
150
+ export async function signCallbackJwt(privateJwk, opts) {
151
+ return new SignJWT({})
152
+ .setProtectedHeader({
153
+ alg: A2A_JWS_ALG,
154
+ kid: privateJwk.kid,
155
+ jku: opts.jku
156
+ })
157
+ .setAudience(opts.aud)
158
+ .setIssuedAt()
159
+ .setExpirationTime(CALLBACK_TOKEN_TTL)
160
+ .sign(privateJwk);
161
+ }
162
+ /**
163
+ * POST a Task snapshot to the gatekeeper's push-notification webhook, wrapped in the
164
+ * v1.0 `StreamResponse` envelope. Returns the raw `Response` so the caller (the
165
+ * workflow's `notify` step) can decide whether a non-2xx warrants a retry.
166
+ */
167
+ export async function postNotification(url, token, jwt, task) {
168
+ const { body, contentType } = PUSH_SERIALIZER.serialize({
169
+ payload: { $case: "task", value: task }
170
+ });
171
+ return fetch(url, {
172
+ method: "POST",
173
+ headers: {
174
+ "content-type": contentType,
175
+ authorization: `Bearer ${jwt}`,
176
+ [NOTIFICATION_TOKEN_HEADER]: token
177
+ },
178
+ body
179
+ });
180
+ }
@@ -0,0 +1,45 @@
1
+ import { type Message, type Part } from "@a2a-js/sdk";
2
+ import type { PlainMessage, PlainPart } from "./task.js";
3
+ /**
4
+ * A `text` part carrying `text`. Typed as the narrowed {@link PlainPart} (which
5
+ * widens to `Part` for free) so a message built here can cross the DO RPC
6
+ * boundary — see {@link file://./task.ts}.
7
+ */
8
+ export declare function textPart(text: string): PlainPart;
9
+ /** Concatenate the text of every `text` part, trimming surrounding whitespace. */
10
+ export declare function partsText(parts: Part[] | undefined): string;
11
+ /** The plain-text content of an inbound A2A message (what the caller said). */
12
+ export declare function textOf(message: Message): string;
13
+ /**
14
+ * Bounds the inbound user text carried in a durable Workflow payload (UTF-8).
15
+ * Workflow params have a platform size limit, and a caller's message is the one
16
+ * unbounded thing that goes into them.
17
+ */
18
+ export declare const MAX_INBOUND_TEXT_BYTES: number;
19
+ /** Invalid inbound content that must not cross the A2A-to-workflow boundary. */
20
+ export declare class InboundPartError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ /**
24
+ * Extract and validate the user-turn text. Rejects a message with no usable
25
+ * text, and enforces a single UTF-8 size bound *before* the text enters a
26
+ * workflow payload — past that point the failure is a workflow that will not
27
+ * start, with no request left to answer on.
28
+ *
29
+ * File and data parts are deliberately out of scope: only text crosses into the
30
+ * agent runtime.
31
+ */
32
+ export declare function inboundText(message: Message): string;
33
+ /**
34
+ * An `agent`-role message carrying one text part, with the proto-required
35
+ * fields this agent never sets filled in. `messageId` is the gatekeeper's dedupe
36
+ * key, so callers pass a **stable** id (never a fresh random per attempt) — a
37
+ * callback the workflow/DO re-runs must reuse the same id or the gatekeeper
38
+ * treats the replay as a new message and double-posts.
39
+ */
40
+ export declare function agentTextMessage(input: {
41
+ messageId: string;
42
+ text: string;
43
+ contextId: string;
44
+ taskId: string;
45
+ }): PlainMessage;
@@ -0,0 +1,94 @@
1
+ import { Role } from "@a2a-js/sdk";
2
+ /**
3
+ * Constructors and readers for A2A v1.0 message content — the one place the
4
+ * adapter reaches into the `@a2a-js/sdk` message shape. Everything past
5
+ * {@link file://./executor.ts A2AExecutor} works in plain strings, so the agent
6
+ * runtime never sees an A2A type.
7
+ *
8
+ * The v1.0 data model is generated from the protobuf schema, so the wire types
9
+ * are "all fields present": a `Part` carries a `content` oneof discriminated by
10
+ * `$case` (plus `filename`/`mediaType`) instead of v0.3's `kind`, and a
11
+ * `Message` carries empty strings and empty arrays rather than omitted
12
+ * optionals. Hand-writing those literals at every call site is noisy and easy to
13
+ * get subtly wrong, so every part/message this agent emits is built here.
14
+ */
15
+ /** Media type stamped on the text parts this agent emits. */
16
+ const TEXT_MEDIA_TYPE = "text/plain";
17
+ /**
18
+ * A `text` part carrying `text`. Typed as the narrowed {@link PlainPart} (which
19
+ * widens to `Part` for free) so a message built here can cross the DO RPC
20
+ * boundary — see {@link file://./task.ts}.
21
+ */
22
+ export function textPart(text) {
23
+ return {
24
+ content: { $case: "text", value: text },
25
+ metadata: undefined,
26
+ filename: "",
27
+ mediaType: TEXT_MEDIA_TYPE
28
+ };
29
+ }
30
+ /** Concatenate the text of every `text` part, trimming surrounding whitespace. */
31
+ export function partsText(parts) {
32
+ let out = "";
33
+ for (const part of parts ?? []) {
34
+ if (part.content?.$case === "text")
35
+ out += part.content.value;
36
+ }
37
+ return out.trim();
38
+ }
39
+ /** The plain-text content of an inbound A2A message (what the caller said). */
40
+ export function textOf(message) {
41
+ return partsText(message.parts);
42
+ }
43
+ /**
44
+ * Bounds the inbound user text carried in a durable Workflow payload (UTF-8).
45
+ * Workflow params have a platform size limit, and a caller's message is the one
46
+ * unbounded thing that goes into them.
47
+ */
48
+ export const MAX_INBOUND_TEXT_BYTES = 256 * 1024;
49
+ const encoder = new TextEncoder();
50
+ /** Invalid inbound content that must not cross the A2A-to-workflow boundary. */
51
+ export class InboundPartError extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "InboundPartError";
55
+ }
56
+ }
57
+ /**
58
+ * Extract and validate the user-turn text. Rejects a message with no usable
59
+ * text, and enforces a single UTF-8 size bound *before* the text enters a
60
+ * workflow payload — past that point the failure is a workflow that will not
61
+ * start, with no request left to answer on.
62
+ *
63
+ * File and data parts are deliberately out of scope: only text crosses into the
64
+ * agent runtime.
65
+ */
66
+ export function inboundText(message) {
67
+ const text = textOf(message);
68
+ if (!text) {
69
+ throw new InboundPartError("message has no usable text");
70
+ }
71
+ if (encoder.encode(text).byteLength > MAX_INBOUND_TEXT_BYTES) {
72
+ throw new InboundPartError("message text exceeds the size limit");
73
+ }
74
+ return text;
75
+ }
76
+ /**
77
+ * An `agent`-role message carrying one text part, with the proto-required
78
+ * fields this agent never sets filled in. `messageId` is the gatekeeper's dedupe
79
+ * key, so callers pass a **stable** id (never a fresh random per attempt) — a
80
+ * callback the workflow/DO re-runs must reuse the same id or the gatekeeper
81
+ * treats the replay as a new message and double-posts.
82
+ */
83
+ export function agentTextMessage(input) {
84
+ return {
85
+ messageId: input.messageId,
86
+ role: Role.ROLE_AGENT,
87
+ parts: [textPart(input.text)],
88
+ contextId: input.contextId,
89
+ taskId: input.taskId,
90
+ metadata: undefined,
91
+ extensions: [],
92
+ referenceTaskIds: []
93
+ };
94
+ }
@@ -0,0 +1,70 @@
1
+ import type { Task } from "@a2a-js/sdk";
2
+ import type { OnContent } from "../agent/inference.js";
3
+ /**
4
+ * The gatekeeper callback channel for one accepted turn.
5
+ *
6
+ * Every agent that accepts asynchronously has to do the same four things with
7
+ * the push config the gatekeeper handed it: sign a callback JWT against the
8
+ * deployment's card key, POST `working` snapshots as the model reasons, POST the
9
+ * terminal Task, and never let a failed progress post take down a turn. That was
10
+ * written out longhand in four places across two agents in the starter — twice as
11
+ * a `streamWorking` closure inside a Durable Object, twice as a `notify` step
12
+ * inside a Workflow — with the signing, the header names and the swallow-and-log
13
+ * discipline duplicated each time.
14
+ *
15
+ * It is one object here because none of it is per-agent. What *is* per-agent is
16
+ * the notification **key**, and that is the one thing a caller supplies.
17
+ */
18
+ /**
19
+ * Everything needed to call a gatekeeper back about one task.
20
+ *
21
+ * RPC-serializable by construction: it crosses the Workflow → Durable Object
22
+ * boundary on every turn, so it holds only strings.
23
+ */
24
+ export interface TurnPushContext {
25
+ /** The accepted task id (echoed on every callback of this turn). */
26
+ taskId: string;
27
+ /** A2A context id, echoed on every callback. */
28
+ contextId: string;
29
+ /** Gatekeeper push-notification webhook (also the callback JWT `aud`). */
30
+ pushUrl: string;
31
+ /** Per-task validation token the gatekeeper set; echoed in the callback header. */
32
+ pushToken: string;
33
+ /** This agent's card-signing JWKS URL — the callback JWT `jku` (pinned key). */
34
+ jku: string;
35
+ }
36
+ export interface PushChannel {
37
+ /**
38
+ * POST one `working` Task snapshot, keyed by a stable semantic `key`.
39
+ *
40
+ * **Best-effort**: every failure is logged and swallowed, so a progress post
41
+ * can never abort generation or fail a durable step. The key is what lets the
42
+ * gatekeeper dedupe a re-posted event on replay, so it must be derived from
43
+ * position (`r2:step:3`), never from content or a clock.
44
+ */
45
+ working(text: string, key: string): Promise<void>;
46
+ /**
47
+ * An {@link OnContent} sink that posts each intermediate message as a
48
+ * `working` snapshot, signing the JWT once and reusing it across the turn.
49
+ *
50
+ * `key` maps a 0-based step ordinal to its notification key. An agent running
51
+ * one turn per task can use the bare index; an agent with rounds **must**
52
+ * include the round, or two rounds of one task collide on the gatekeeper.
53
+ */
54
+ stream(key: (stepIndex: number) => string): OnContent;
55
+ /**
56
+ * POST the terminal Task. **Throws on a non-2xx** so the calling Workflow step
57
+ * retries — the opposite of {@link working}, because this is the delivery the
58
+ * whole turn exists for.
59
+ */
60
+ deliver(task: Task): Promise<void>;
61
+ }
62
+ /**
63
+ * Build the callback channel for one turn.
64
+ *
65
+ * `signingKey` is the deployment's Ed25519 private JWK as JSON — `A2A_SIGNING_KEY`
66
+ * by default. There is one per origin, not one per agent: the card sits at a
67
+ * well-known URI, which RFC 8615 defines per-authority, so this origin publishes
68
+ * one card and the gatekeeper pins one key for every agent on it.
69
+ */
70
+ export declare function createPushChannel(signingKey: string, push: TurnPushContext): PushChannel;
@@ -0,0 +1,53 @@
1
+ import { parsePrivateJwk } from "./card.js";
2
+ import { buildWorkingTask, postNotification, signCallbackJwt } from "./notify.js";
3
+ /**
4
+ * Build the callback channel for one turn.
5
+ *
6
+ * `signingKey` is the deployment's Ed25519 private JWK as JSON — `A2A_SIGNING_KEY`
7
+ * by default. There is one per origin, not one per agent: the card sits at a
8
+ * well-known URI, which RFC 8615 defines per-authority, so this origin publishes
9
+ * one card and the gatekeeper pins one key for every agent on it.
10
+ */
11
+ export function createPushChannel(signingKey, push) {
12
+ // Signed lazily and reused: one turn can post many progress snapshots, and
13
+ // re-signing per message costs a key import each time for no benefit.
14
+ let jwt;
15
+ const sign = () => (jwt ??= signCallbackJwt(parsePrivateJwk(signingKey), {
16
+ jku: push.jku,
17
+ aud: push.pushUrl
18
+ }));
19
+ const post = async (task) => postNotification(push.pushUrl, push.pushToken, await sign(), task);
20
+ const working = async (text, key) => {
21
+ try {
22
+ const res = await post(buildWorkingTask(push.taskId, push.contextId, text, key));
23
+ if (!res.ok) {
24
+ console.warn("[push] working notification non-2xx", {
25
+ taskId: push.taskId,
26
+ key,
27
+ status: res.status
28
+ });
29
+ }
30
+ }
31
+ catch (err) {
32
+ // Swallowed on purpose, including a signing failure: a turn that cannot
33
+ // report its progress is still a turn that should deliver its answer.
34
+ console.warn("[push] working notification failed", {
35
+ taskId: push.taskId,
36
+ key,
37
+ err: String(err)
38
+ });
39
+ }
40
+ };
41
+ return {
42
+ working,
43
+ stream(key) {
44
+ return (text, stepIndex) => working(text, key(stepIndex));
45
+ },
46
+ async deliver(task) {
47
+ const res = await post(task);
48
+ if (!res.ok) {
49
+ throw new Error(`gatekeeper notification failed: HTTP ${res.status}`);
50
+ }
51
+ }
52
+ };
53
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * This deployment's own public origin — learned from the request path, never
3
+ * configured.
4
+ *
5
+ * ## Why an agent needs it at all
6
+ *
7
+ * A Worker that only *answers* never needs to know its own name.
8
+ * {@link file://../worker/index.ts createA2AWorker} derives its audience, its
9
+ * card and its `jku` from `new URL(request.url).origin`, and none of it outlives
10
+ * the request. An agent that **calls out** mint-signed does need it:
11
+ * {@link file://./caller-token.ts signCallerToken} puts it in `iss` and derives
12
+ * the token's `jku` from it. That call happens inside a Durable Object, where
13
+ * there is no `Request` — which is the whole difficulty.
14
+ *
15
+ * The obvious answer is a `SELF_ORIGIN` secret, and it is the wrong one. It
16
+ * restates a value the request already carries, and it has to be kept
17
+ * byte-identical by hand with the origin allowlist on the far side, in every
18
+ * environment, forever. Every sibling that tried it took it back out —
19
+ * `slack-gatekeeper` discovers its own origin from the first signature-verified
20
+ * request rather than being told, and a verifying Worker that once carried a
21
+ * configured audience replaced it with `url.origin`.
22
+ *
23
+ * ## Where the value comes from
24
+ *
25
+ * Core already sends the origin into the Durable Object on every turn, one field
26
+ * short of this use. `A2AExecutor` computes `jku` as `${origin}${jwksPath}` and
27
+ * it rides {@link file://./push.ts TurnPushContext} through the Workflow into
28
+ * `runTaskTurn` and `executeSubtaskChunk`.
29
+ *
30
+ * That is the same origin `signCallerToken` needs, and not by coincidence: a
31
+ * caller token's `jku` **must** be the JWKS the verifier fetches, and `iss` must
32
+ * agree with it — the third check in {@link file://./verify.ts verify.ts}. An
33
+ * origin derived from anywhere else is exactly what that check exists to catch,
34
+ * so deriving it from the `jku` core already serves makes the agreement
35
+ * structural instead of clerical.
36
+ *
37
+ * ## Pinned on the first turn, and in memory
38
+ *
39
+ * The first origin an isolate is told wins, and later ones are ignored. That is
40
+ * not laziness about staleness — it is what makes the value safe to *read*.
41
+ *
42
+ * A Durable Object's input gate stays open across a non-storage await, and this
43
+ * package runs concurrent RPCs into one object by design (`round/workflow.ts`
44
+ * runs a round's branches under `Promise.all`). Mutable instance state can
45
+ * therefore change while a turn is awaiting a model call, and the credential
46
+ * thunks that read this are lazy — they run several frames below the turn, when
47
+ * the client is built. Pinned, the field is immutable after its first write, so
48
+ * every concurrent reader in the isolate gets the same string and no turn can
49
+ * sign as another turn's origin.
50
+ *
51
+ * The cost of pinning is what an agent does not have: several identities. An
52
+ * agent has one endpoint — the one its card advertises, the one a gatekeeper calls
53
+ * and a verifier allowlists — so there is nothing to follow. Note the asymmetry
54
+ * with a *verifier*, which derives the audience it expects per request and must
55
+ * not cache it: a verifier has to accept every hostname it answers on, while a
56
+ * signer needs one stable identity.
57
+ *
58
+ * Nothing is persisted, which is what keeps a pin from outliving its truth. An
59
+ * isolate is fresh on every `wrangler deploy` and recycles on its own, so a moved
60
+ * deployment re-learns its origin from the next turn it serves.
61
+ */
62
+ export declare class SelfOrigin {
63
+ private observed?;
64
+ /**
65
+ * Offer the origin of an absolute URL seen on the request path — a `jku`, an
66
+ * endpoint, or a bare origin. Only `.origin` is kept, so a path or a trailing
67
+ * slash cannot reach a token claim.
68
+ *
69
+ * **The first usable value wins**; every later call is a no-op, including one
70
+ * naming a different origin. Called at each RPC entry and again when the push
71
+ * channel is built, so most calls are already no-ops — but the reason for the
72
+ * pin is the read side, not the write side. See the note above the class.
73
+ *
74
+ * Silently ignores anything unusable (absent, relative, or a scheme that has
75
+ * no meaningful origin), and an unusable value never pins: the parse comes
76
+ * first and the field is assigned only on success. This runs at the top of a
77
+ * turn, where a diagnostic value must never be the thing that fails it; the
78
+ * throw belongs at {@link require}, where something actually wanted the value.
79
+ */
80
+ note(url: string | undefined): void;
81
+ /** The pinned origin, or `undefined` when nothing has carried one yet. */
82
+ peek(): string | undefined;
83
+ /**
84
+ * The pinned origin, for a caller that cannot proceed without it.
85
+ *
86
+ * Throws naming the timing, because that is what the mistake always is: the
87
+ * value arrives with a turn, so `onStart`, a constructor and a scheduled
88
+ * callback all run before any request has said what this deployment is called.
89
+ */
90
+ require(): string;
91
+ }