@tangle-network/agent-app 0.43.76 → 0.44.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +1 -1
  2. package/dist/DesignCanvas-Z362I3R3.js +10 -0
  3. package/dist/{DesignCanvasEditor-FJIVYOA2.js → DesignCanvasEditor-ZFE2W4OJ.js} +4 -4
  4. package/dist/assets/index.d.ts +4 -4
  5. package/dist/assets/index.js +223 -11
  6. package/dist/assets/index.js.map +1 -1
  7. package/dist/assistant/index.d.ts +7 -7
  8. package/dist/assistant/index.js +3 -3
  9. package/dist/assistant/index.js.map +1 -1
  10. package/dist/{attachment-validation-Dv1A_Puy.d.ts → attachment-validation-DRuLEjAE.d.ts} +1 -1
  11. package/dist/{auth-_FU8w01b.d.ts → auth-anc7mv2W.d.ts} +2 -2
  12. package/dist/chat-routes/index.d.ts +14 -11
  13. package/dist/chat-routes/index.js +16 -17
  14. package/dist/chat-routes/index.js.map +1 -1
  15. package/dist/chat-store/index.d.ts +26 -7
  16. package/dist/chat-store/index.js +14 -5
  17. package/dist/chat-store/index.js.map +1 -1
  18. package/dist/{chunk-ROLFDOMJ.js → chunk-4TI7QWLW.js} +3 -3
  19. package/dist/{chunk-3JUTOVUH.js → chunk-6KMAM7BL.js} +1 -1
  20. package/dist/chunk-6KMAM7BL.js.map +1 -0
  21. package/dist/{chunk-WL7XHLDK.js → chunk-ICOHEZK6.js} +77 -5
  22. package/dist/chunk-ICOHEZK6.js.map +1 -0
  23. package/dist/{chunk-22GLESKE.js → chunk-O63WWYET.js} +2 -4
  24. package/dist/chunk-O63WWYET.js.map +1 -0
  25. package/dist/{chunk-E7N67FYI.js → chunk-Q3FONVAK.js} +5 -5
  26. package/dist/{chunk-E7N67FYI.js.map → chunk-Q3FONVAK.js.map} +1 -1
  27. package/dist/{chunk-RXOTWZ4G.js → chunk-TKRI463Z.js} +4 -4
  28. package/dist/chunk-TKRI463Z.js.map +1 -0
  29. package/dist/config/index.d.ts +1 -1
  30. package/dist/config/index.js +123 -4
  31. package/dist/config/index.js.map +1 -1
  32. package/dist/{contract-B3h7peV3.d.ts → contract-CEewO6DI.d.ts} +1 -1
  33. package/dist/design-canvas/index.d.ts +4 -4
  34. package/dist/design-canvas-react/index.js +7 -7
  35. package/dist/design-canvas-react/lazy.js +1 -1
  36. package/dist/eval/index.d.ts +1 -1
  37. package/dist/eval/index.js +22 -8
  38. package/dist/eval/index.js.map +1 -1
  39. package/dist/{flow-types-CqomVAUN.d.ts → flow-types-CJxEmaRy.d.ts} +1 -1
  40. package/dist/integrations/index.js +67 -5
  41. package/dist/integrations/index.js.map +1 -1
  42. package/dist/interactions/index.d.ts +6 -4
  43. package/dist/interactions/index.js +1 -2
  44. package/dist/knowledge/index.js +63 -4
  45. package/dist/knowledge/index.js.map +1 -1
  46. package/dist/knowledge-loop/index.d.ts +1 -1
  47. package/dist/knowledge-loop/index.js +94 -4
  48. package/dist/knowledge-loop/index.js.map +1 -1
  49. package/dist/{mcp-BjPqQ-3C.d.ts → mcp-D3qVzbE1.d.ts} +2 -2
  50. package/dist/{mcp-rpc-BfUOsalw.d.ts → mcp-rpc-CzU5LWWT.d.ts} +1 -1
  51. package/dist/missions/index.js +1143 -23
  52. package/dist/missions/index.js.map +1 -1
  53. package/dist/{model-DmdkIteM.d.ts → model-CdCDfBA9.d.ts} +1 -1
  54. package/dist/object-store/index.js +113 -8
  55. package/dist/object-store/index.js.map +1 -1
  56. package/dist/{parts-2ymE5cs-.d.ts → parts-Co_lFgLI.d.ts} +3 -3
  57. package/dist/platform/index.d.ts +1 -1
  58. package/dist/preflight/cli.d.ts +13 -0
  59. package/dist/preflight/cli.js +50 -0
  60. package/dist/preflight/cli.js.map +1 -0
  61. package/dist/preset-cloudflare/index.d.ts +1 -1
  62. package/dist/preset-cloudflare/index.js +284 -11
  63. package/dist/preset-cloudflare/index.js.map +1 -1
  64. package/dist/{queue-C24V13h9.d.ts → queue-CD5-_VTX.d.ts} +1 -1
  65. package/dist/redact/index.js +116 -8
  66. package/dist/redact/index.js.map +1 -1
  67. package/dist/runtime/index.d.ts +19 -111
  68. package/dist/runtime/index.js +234 -14
  69. package/dist/runtime/index.js.map +1 -1
  70. package/dist/sandbox/index.d.ts +3 -3
  71. package/dist/sandbox/index.js +2 -3
  72. package/dist/sequences/index.d.ts +4 -4
  73. package/dist/stream/index.d.ts +4 -4
  74. package/dist/{stream-normalizer-QYUl4vnl.d.ts → stream-normalizer-DnuqkZvw.d.ts} +2 -2
  75. package/dist/tangle/index.js +42 -4
  76. package/dist/tangle/index.js.map +1 -1
  77. package/dist/teams/index.js +5 -5
  78. package/dist/teams/invitations-api.js +4 -4
  79. package/dist/teams-react/index.js +3 -3
  80. package/dist/theme/index.js +0 -1
  81. package/dist/tools/index.d.ts +6 -6
  82. package/dist/tools/index.js +5 -7
  83. package/dist/trace/index.d.ts +1 -1
  84. package/dist/trace/index.js +179 -10
  85. package/dist/trace/index.js.map +1 -1
  86. package/dist/{turn-buffer-BaSgxVmK.d.ts → turn-buffer-BcPfhr1_.d.ts} +1 -1
  87. package/dist/turn-stream/index.d.ts +1 -1
  88. package/dist/{types-CBRyqijY.d.ts → types-BCxK0wyS.d.ts} +1 -1
  89. package/dist/{types-CEchbvgz.d.ts → types-XIZqIdX1.d.ts} +1 -1
  90. package/dist/web-react/index.d.ts +10 -10
  91. package/dist/web-react/index.js +5 -5
  92. package/dist/work-product/index.d.ts +4 -4
  93. package/dist/work-product/index.js +735 -15
  94. package/dist/work-product/index.js.map +1 -1
  95. package/dist/work-product-react/index.d.ts +1 -1
  96. package/dist/work-product-react/index.js.map +1 -1
  97. package/package.json +4 -19
  98. package/dist/DesignCanvas-UNL34LS2.js +0 -12
  99. package/dist/chunk-22GLESKE.js.map +0 -1
  100. package/dist/chunk-3JUTOVUH.js.map +0 -1
  101. package/dist/chunk-5EQCITY3.js +0 -20
  102. package/dist/chunk-5EQCITY3.js.map +0 -1
  103. package/dist/chunk-5SD6HT2V.js +0 -236
  104. package/dist/chunk-5SD6HT2V.js.map +0 -1
  105. package/dist/chunk-5TESA4SN.js +0 -74
  106. package/dist/chunk-5TESA4SN.js.map +0 -1
  107. package/dist/chunk-6GQ5DKFK.js +0 -920
  108. package/dist/chunk-6GQ5DKFK.js.map +0 -1
  109. package/dist/chunk-7ESQUSAC.js +0 -343
  110. package/dist/chunk-7ESQUSAC.js.map +0 -1
  111. package/dist/chunk-7LNGJDNA.js +0 -1
  112. package/dist/chunk-7LNGJDNA.js.map +0 -1
  113. package/dist/chunk-AP2UXKUK.js +0 -69
  114. package/dist/chunk-AP2UXKUK.js.map +0 -1
  115. package/dist/chunk-FA4XR66Y.js +0 -32
  116. package/dist/chunk-FA4XR66Y.js.map +0 -1
  117. package/dist/chunk-FKCB5FTD.js +0 -126
  118. package/dist/chunk-FKCB5FTD.js.map +0 -1
  119. package/dist/chunk-GPBRL2XR.js +0 -102
  120. package/dist/chunk-GPBRL2XR.js.map +0 -1
  121. package/dist/chunk-HRH7ASAG.js +0 -759
  122. package/dist/chunk-HRH7ASAG.js.map +0 -1
  123. package/dist/chunk-HYTIHYNV.js +0 -297
  124. package/dist/chunk-HYTIHYNV.js.map +0 -1
  125. package/dist/chunk-LGBO24FL.js +0 -190
  126. package/dist/chunk-LGBO24FL.js.map +0 -1
  127. package/dist/chunk-NLSSFLLS.js +0 -1168
  128. package/dist/chunk-NLSSFLLS.js.map +0 -1
  129. package/dist/chunk-OSMNYPOG.js +0 -129
  130. package/dist/chunk-OSMNYPOG.js.map +0 -1
  131. package/dist/chunk-QYOD3K56.js +0 -124
  132. package/dist/chunk-QYOD3K56.js.map +0 -1
  133. package/dist/chunk-RXOTWZ4G.js.map +0 -1
  134. package/dist/chunk-UAKFCMWK.js +0 -1
  135. package/dist/chunk-UAKFCMWK.js.map +0 -1
  136. package/dist/chunk-UKTYKG57.js +0 -48
  137. package/dist/chunk-UKTYKG57.js.map +0 -1
  138. package/dist/chunk-UOAYS72M.js +0 -80
  139. package/dist/chunk-UOAYS72M.js.map +0 -1
  140. package/dist/chunk-WL7XHLDK.js.map +0 -1
  141. package/dist/composer/index.d.ts +0 -2
  142. package/dist/composer/index.js +0 -22
  143. package/dist/composer/index.js.map +0 -1
  144. package/dist/core-7qIM7svy.d.ts +0 -21
  145. package/dist/durable-chat/index.d.ts +0 -464
  146. package/dist/durable-chat/index.js +0 -59
  147. package/dist/durable-chat/index.js.map +0 -1
  148. package/dist/index.d.ts +0 -51
  149. package/dist/index.js +0 -827
  150. package/dist/index.js.map +0 -1
  151. /package/dist/{DesignCanvas-UNL34LS2.js.map → DesignCanvas-Z362I3R3.js.map} +0 -0
  152. /package/dist/{DesignCanvasEditor-FJIVYOA2.js.map → DesignCanvasEditor-ZFE2W4OJ.js.map} +0 -0
  153. /package/dist/{chunk-ROLFDOMJ.js.map → chunk-4TI7QWLW.js.map} +0 -0
@@ -1,26 +1,1146 @@
1
- import {
2
- DEFAULT_MISSION_STEP_KINDS,
3
- MISSION_CONTROL_CHANNEL_ID,
4
- MissionConcurrencyError,
5
- RetryableStepError,
6
- applyMissionEvent,
7
- asMissionStreamEvent,
8
- budgetGateProposalId,
9
- buildAgentMissionPlan,
10
- createInMemoryMissionStore,
11
- createMissionEngine,
12
- createMissionService,
13
- isMissionStopRequested,
14
- isMissionTerminal,
15
- mergeMissionState,
16
- noopEventSink,
17
- parseMissionBlocks,
18
- parseSessionStreamEnvelope,
19
- reduceMissionEvents,
20
- stepAgentActivity,
21
- stepGateProposalId,
22
- volumeGateProposalId
23
- } from "../chunk-NLSSFLLS.js";
1
+ // src/missions/service.ts
2
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
3
+ "succeeded",
4
+ "failed",
5
+ "aborted",
6
+ "cancelled"
7
+ ]);
8
+ function isMissionTerminal(status) {
9
+ return TERMINAL_STATUSES.has(status);
10
+ }
11
+ function isMissionStopRequested(mission) {
12
+ return (mission.metadata ?? {}).stopRequested === true;
13
+ }
14
+ var MISSION_TRANSITIONS = {
15
+ scheduled: /* @__PURE__ */ new Set(["running", "cancelled", "aborted"]),
16
+ running: /* @__PURE__ */ new Set([
17
+ "paused",
18
+ "waiting_approval",
19
+ "blocked",
20
+ "succeeded",
21
+ "failed",
22
+ "aborted"
23
+ ]),
24
+ paused: /* @__PURE__ */ new Set(["running", "aborted", "cancelled"]),
25
+ waiting_approval: /* @__PURE__ */ new Set(["running", "aborted", "cancelled"]),
26
+ blocked: /* @__PURE__ */ new Set(["running", "aborted", "cancelled"]),
27
+ succeeded: /* @__PURE__ */ new Set(),
28
+ failed: /* @__PURE__ */ new Set(),
29
+ aborted: /* @__PURE__ */ new Set(),
30
+ cancelled: /* @__PURE__ */ new Set()
31
+ };
32
+ var STEP_TRANSITIONS = {
33
+ pending: /* @__PURE__ */ new Set(["running", "waiting_approval", "failed"]),
34
+ running: /* @__PURE__ */ new Set(["done", "failed", "waiting_approval"]),
35
+ waiting_approval: /* @__PURE__ */ new Set(["running", "done", "failed"]),
36
+ done: /* @__PURE__ */ new Set([]),
37
+ failed: /* @__PURE__ */ new Set(["pending", "running"])
38
+ };
39
+ var ZERO_LEDGER = {
40
+ tokensIn: 0,
41
+ tokensOut: 0,
42
+ costUsd: 0,
43
+ wallMs: 0,
44
+ llmCalls: 0
45
+ };
46
+ function rejected(error) {
47
+ return { succeeded: false, error, conflict: false };
48
+ }
49
+ function lostRace(id) {
50
+ return { succeeded: false, error: `Mission ${id} changed concurrently`, conflict: true };
51
+ }
52
+ function createMissionService(options) {
53
+ const { store } = options;
54
+ const now = options.now ?? (() => Date.now());
55
+ const generateId = options.generateId ?? (() => crypto.randomUUID());
56
+ async function appendEvent(mission, level, step, message, metadata = {}) {
57
+ await store.appendEvent({
58
+ missionId: mission.id,
59
+ workspaceId: mission.workspaceId,
60
+ level,
61
+ step,
62
+ message,
63
+ metadata,
64
+ at: now()
65
+ });
66
+ }
67
+ async function transition(id, to, patch = {}, eventMeta = {}) {
68
+ const mission = await store.load(id);
69
+ if (!mission) return rejected(`Mission ${id} not found`);
70
+ const from = mission.status;
71
+ if (TERMINAL_STATUSES.has(from)) {
72
+ return rejected(`Mission ${id} is terminal (${from}); cannot transition to ${to}`);
73
+ }
74
+ if (from === to) return { succeeded: true, value: mission };
75
+ if (!MISSION_TRANSITIONS[from].has(to)) {
76
+ return rejected(`Illegal mission transition ${from} -> ${to} for mission ${id}`);
77
+ }
78
+ const updated = await store.update(id, { status: from }, { status: to, ...patch });
79
+ if (!updated) return lostRace(id);
80
+ await appendEvent(updated, to === "failed" ? "error" : "info", `mission.${to}`, `Mission ${from} -> ${to}`, {
81
+ from,
82
+ to,
83
+ ...eventMeta
84
+ });
85
+ return { succeeded: true, value: updated };
86
+ }
87
+ const createMission = async (input) => {
88
+ const seen = /* @__PURE__ */ new Set();
89
+ for (const step of input.plan) {
90
+ if (seen.has(step.id)) {
91
+ throw new Error(`Duplicate plan step id "${step.id}" \u2014 mission plan step ids must be unique`);
92
+ }
93
+ seen.add(step.id);
94
+ }
95
+ const scheduledAt = input.scheduledAt ?? null;
96
+ const status = scheduledAt !== null ? "scheduled" : "running";
97
+ const plan = input.plan.map((step) => ({
98
+ id: step.id,
99
+ intent: step.intent,
100
+ kind: step.kind,
101
+ status: step.status,
102
+ attempts: step.attempts,
103
+ ...step.sublabel === void 0 ? {} : { sublabel: step.sublabel },
104
+ ...step.resultRef === void 0 ? {} : { resultRef: step.resultRef }
105
+ }));
106
+ const record = await store.insert(
107
+ {
108
+ id: input.id ?? generateId(),
109
+ workspaceId: input.workspaceId,
110
+ status,
111
+ trigger: input.trigger,
112
+ summary: input.title,
113
+ plan,
114
+ cursor: 0,
115
+ cost: { ...ZERO_LEDGER },
116
+ budgetUsd: input.budgetUsd ?? null,
117
+ spentUsd: 0,
118
+ pauseReason: null,
119
+ engineRef: null,
120
+ scheduledAt,
121
+ startedAt: now(),
122
+ completedAt: null,
123
+ metadata: input.metadata ?? null
124
+ },
125
+ input.extras
126
+ );
127
+ await appendEvent(record, "info", "mission.created", `Mission "${input.title}" ${status}`, {
128
+ status,
129
+ stepCount: plan.length,
130
+ budgetUsd: input.budgetUsd ?? null,
131
+ scheduledAt
132
+ });
133
+ return record;
134
+ };
135
+ const getMission = (id) => store.load(id);
136
+ const setEngineRef = async (id, engineRef) => {
137
+ const mission = await store.load(id);
138
+ if (!mission) return rejected(`Mission ${id} not found`);
139
+ if (TERMINAL_STATUSES.has(mission.status) || isMissionStopRequested(mission)) {
140
+ return rejected(`Mission ${id} is not writable in status ${mission.status}`);
141
+ }
142
+ if (mission.engineRef === engineRef) return { succeeded: true, value: mission };
143
+ if (mission.engineRef !== null) {
144
+ return rejected(`Mission ${id} is already bound to engine ${mission.engineRef}`);
145
+ }
146
+ const updated = await store.update(id, { engineRefIsNull: true }, { engineRef });
147
+ if (!updated) return lostRace(id);
148
+ await appendEvent(updated, "info", "mission.engine", `Engine bound: ${engineRef}`, { engineRef });
149
+ return { succeeded: true, value: updated };
150
+ };
151
+ const mergeMetadata = async (id, patch) => {
152
+ const mission = await store.load(id);
153
+ if (!mission) return rejected(`Mission ${id} not found`);
154
+ if (TERMINAL_STATUSES.has(mission.status) || isMissionStopRequested(mission)) {
155
+ return rejected(`Mission ${id} is not writable in status ${mission.status}`);
156
+ }
157
+ const updated = await store.update(
158
+ id,
159
+ { metadata: mission.metadata },
160
+ { metadata: { ...mission.metadata ?? {}, ...patch } }
161
+ );
162
+ if (!updated) return lostRace(id);
163
+ return { succeeded: true, value: updated };
164
+ };
165
+ const setStepStatus = async (id, stepId, status, patch = {}) => {
166
+ const mission = await store.load(id);
167
+ if (!mission) return rejected(`Mission ${id} not found`);
168
+ const plan = mission.plan;
169
+ const index = plan.findIndex((step) => step.id === stepId);
170
+ const current = index < 0 ? void 0 : plan[index];
171
+ if (!current) return rejected(`Step ${stepId} not found in mission ${id}`);
172
+ const sameStatus = current.status === status;
173
+ if (!sameStatus && !STEP_TRANSITIONS[current.status].has(status)) {
174
+ return rejected(`Illegal step transition ${current.status} -> ${status} for step ${stepId}`);
175
+ }
176
+ const sublabelChanges = patch.sublabel !== void 0 && patch.sublabel !== current.sublabel;
177
+ const resultRefChanges = patch.resultRef !== void 0 && patch.resultRef !== current.resultRef;
178
+ if (sameStatus && !sublabelChanges && !resultRefChanges) {
179
+ return { succeeded: true, value: mission };
180
+ }
181
+ const nextStep = {
182
+ ...current,
183
+ status,
184
+ attempts: status === "running" && !sameStatus ? current.attempts + 1 : current.attempts,
185
+ ...patch.sublabel === void 0 ? {} : { sublabel: patch.sublabel },
186
+ ...patch.resultRef === void 0 ? {} : { resultRef: patch.resultRef }
187
+ };
188
+ const nextPlan = plan.slice();
189
+ nextPlan[index] = nextStep;
190
+ const cost = patch.cost;
191
+ const ledgerDelta = cost?.ledgerDelta;
192
+ const base = mission.cost ?? { ...ZERO_LEDGER };
193
+ const nextCost = cost === void 0 ? void 0 : {
194
+ tokensIn: base.tokensIn + (ledgerDelta?.tokensIn ?? 0),
195
+ tokensOut: base.tokensOut + (ledgerDelta?.tokensOut ?? 0),
196
+ costUsd: base.costUsd + (ledgerDelta?.costUsd ?? cost.deltaUsd),
197
+ wallMs: base.wallMs + (ledgerDelta?.wallMs ?? 0),
198
+ llmCalls: base.llmCalls + (ledgerDelta?.llmCalls ?? 0)
199
+ };
200
+ const updated = await store.update(
201
+ id,
202
+ {
203
+ status: mission.status,
204
+ plan: mission.plan,
205
+ metadata: mission.metadata,
206
+ ...cost === void 0 ? {} : { cost: mission.cost }
207
+ },
208
+ {
209
+ plan: nextPlan,
210
+ ...cost === void 0 ? {} : { cost: nextCost, spentUsd: mission.spentUsd + cost.deltaUsd }
211
+ }
212
+ );
213
+ if (!updated) return lostRace(id);
214
+ await appendEvent(
215
+ updated,
216
+ status === "failed" ? "error" : "info",
217
+ `mission.step.${status}`,
218
+ patch.error ?? `Step ${stepId} (${current.intent}) -> ${status}`,
219
+ {
220
+ stepId,
221
+ from: current.status,
222
+ to: status,
223
+ attempts: nextStep.attempts,
224
+ ...patch.resultRef ? { resultRef: patch.resultRef } : {}
225
+ }
226
+ );
227
+ if (cost !== void 0) {
228
+ await appendEvent(updated, "info", "mission.cost", `Spent +$${cost.deltaUsd.toFixed(4)}`, {
229
+ deltaUsd: cost.deltaUsd,
230
+ spentUsd: updated.spentUsd,
231
+ budgetUsd: updated.budgetUsd
232
+ });
233
+ }
234
+ return { succeeded: true, value: updated };
235
+ };
236
+ const advanceCursor = async (id) => {
237
+ const mission = await store.load(id);
238
+ if (!mission) return rejected(`Mission ${id} not found`);
239
+ const next = mission.cursor + 1;
240
+ if (next > mission.plan.length) {
241
+ return rejected(`Cursor ${mission.cursor} is already at the end of mission ${id}`);
242
+ }
243
+ const updated = await store.update(
244
+ id,
245
+ { status: mission.status, cursor: mission.cursor },
246
+ { cursor: next }
247
+ );
248
+ if (!updated) return lostRace(id);
249
+ await appendEvent(updated, "info", "mission.cursor", `Cursor ${mission.cursor} -> ${updated.cursor}`, {
250
+ from: mission.cursor,
251
+ to: updated.cursor
252
+ });
253
+ return { succeeded: true, value: updated };
254
+ };
255
+ const addCost = async (id, deltaUsd, ledgerDelta) => {
256
+ const mission = await store.load(id);
257
+ if (!mission) return rejected(`Mission ${id} not found`);
258
+ const base = mission.cost ?? { ...ZERO_LEDGER };
259
+ const nextCost = {
260
+ tokensIn: base.tokensIn + (ledgerDelta?.tokensIn ?? 0),
261
+ tokensOut: base.tokensOut + (ledgerDelta?.tokensOut ?? 0),
262
+ costUsd: base.costUsd + (ledgerDelta?.costUsd ?? deltaUsd),
263
+ wallMs: base.wallMs + (ledgerDelta?.wallMs ?? 0),
264
+ llmCalls: base.llmCalls + (ledgerDelta?.llmCalls ?? 0)
265
+ };
266
+ const updated = await store.update(
267
+ id,
268
+ { cost: mission.cost },
269
+ { cost: nextCost, spentUsd: mission.spentUsd + deltaUsd }
270
+ );
271
+ if (!updated) return lostRace(id);
272
+ await appendEvent(updated, "info", "mission.cost", `Spent +$${deltaUsd.toFixed(4)}`, {
273
+ deltaUsd,
274
+ spentUsd: updated.spentUsd,
275
+ budgetUsd: updated.budgetUsd
276
+ });
277
+ return { succeeded: true, value: updated };
278
+ };
279
+ const markWaitingApproval = async (id, stepId) => {
280
+ const mission = await store.load(id);
281
+ if (!mission) return rejected(`Mission ${id} not found`);
282
+ if (!MISSION_TRANSITIONS[mission.status].has("waiting_approval")) {
283
+ return rejected(`Illegal mission transition ${mission.status} -> waiting_approval for mission ${id}`);
284
+ }
285
+ const stepResult = await setStepStatus(id, stepId, "waiting_approval");
286
+ if (!stepResult.succeeded) return stepResult;
287
+ return transition(id, "waiting_approval", {}, { stepId });
288
+ };
289
+ return {
290
+ createMission,
291
+ getMission,
292
+ setEngineRef,
293
+ mergeMetadata,
294
+ setStepStatus,
295
+ advanceCursor,
296
+ addCost,
297
+ markWaitingApproval,
298
+ pause: (id, reason) => transition(id, "paused", { pauseReason: reason }),
299
+ resume: (id) => transition(id, "running", { pauseReason: null }),
300
+ abort: (id) => transition(id, "aborted", { completedAt: now() }),
301
+ complete: (id, input) => transition(id, input.ok ? "succeeded" : "failed", {
302
+ completedAt: now(),
303
+ ...input.summary === void 0 ? {} : { summary: input.summary }
304
+ })
305
+ };
306
+ }
307
+ function createInMemoryMissionStore() {
308
+ const rows = /* @__PURE__ */ new Map();
309
+ const events = [];
310
+ return {
311
+ async load(id) {
312
+ const record = rows.get(id);
313
+ return record ? structuredClone(record) : null;
314
+ },
315
+ async insert(record) {
316
+ if (rows.has(record.id)) throw new Error(`Mission ${record.id} already exists`);
317
+ rows.set(record.id, structuredClone(record));
318
+ return structuredClone(record);
319
+ },
320
+ async update(id, guard, patch) {
321
+ const current = rows.get(id);
322
+ if (!current) return null;
323
+ if (guard.status !== void 0 && current.status !== guard.status) return null;
324
+ if (guard.cursor !== void 0 && current.cursor !== guard.cursor) return null;
325
+ if (guard.plan !== void 0 && JSON.stringify(current.plan) !== JSON.stringify(guard.plan)) return null;
326
+ if (guard.cost !== void 0 && JSON.stringify(current.cost) !== JSON.stringify(guard.cost)) return null;
327
+ if (guard.metadata !== void 0 && JSON.stringify(current.metadata) !== JSON.stringify(guard.metadata)) {
328
+ return null;
329
+ }
330
+ if (guard.engineRefIsNull && current.engineRef !== null) return null;
331
+ const next = { ...current };
332
+ if (patch.status !== void 0) next.status = patch.status;
333
+ if (patch.pauseReason !== void 0) next.pauseReason = patch.pauseReason;
334
+ if (patch.summary !== void 0) next.summary = patch.summary;
335
+ if (patch.completedAt !== void 0) next.completedAt = patch.completedAt;
336
+ if (patch.plan !== void 0) next.plan = patch.plan;
337
+ if (patch.cursor !== void 0) next.cursor = patch.cursor;
338
+ if (patch.cost !== void 0) next.cost = patch.cost;
339
+ if (patch.spentUsd !== void 0) next.spentUsd = patch.spentUsd;
340
+ if (patch.metadata !== void 0) next.metadata = patch.metadata;
341
+ if (patch.engineRef !== void 0) next.engineRef = patch.engineRef;
342
+ rows.set(id, structuredClone(next));
343
+ return structuredClone(next);
344
+ },
345
+ async appendEvent(event) {
346
+ events.push(structuredClone(event));
347
+ },
348
+ events() {
349
+ return events.map((event) => structuredClone(event));
350
+ },
351
+ put(record) {
352
+ rows.set(record.id, structuredClone(record));
353
+ }
354
+ };
355
+ }
356
+
357
+ // src/missions/events.ts
358
+ var noopEventSink = { emit() {
359
+ } };
360
+ var MISSION_CONTROL_CHANNEL_ID = "missions";
361
+ var MISSION_EVENT_TYPES = /* @__PURE__ */ new Set([
362
+ "mission.created",
363
+ "mission.started",
364
+ "step.started",
365
+ "step.updated",
366
+ "step.completed",
367
+ "cost.updated",
368
+ "mission.paused",
369
+ "mission.waiting_approval",
370
+ "mission.resumed",
371
+ "mission.plan.updated",
372
+ "mission.completed"
373
+ ]);
374
+ function parseSessionStreamEnvelope(raw) {
375
+ if (!raw || typeof raw !== "object") return null;
376
+ const envelope = raw;
377
+ if (typeof envelope.type !== "string") return null;
378
+ const data = envelope.data && typeof envelope.data === "object" ? envelope.data : {};
379
+ return asMissionStreamEvent({ ...data, type: envelope.type });
380
+ }
381
+ function asMissionStreamEvent(value) {
382
+ if (!value || typeof value !== "object") return null;
383
+ const record = value;
384
+ const type = record.type;
385
+ if (typeof type !== "string" || !MISSION_EVENT_TYPES.has(type)) return null;
386
+ if (typeof record.missionId !== "string" || !record.missionId) return null;
387
+ return value;
388
+ }
389
+ var STEP_RANK = {
390
+ pending: 0,
391
+ running: 1,
392
+ waiting_approval: 2,
393
+ // done and failed are both TERMINAL for a step; rank them equal-and-highest
394
+ // so neither can be overwritten by the other or regressed to running.
395
+ done: 3,
396
+ failed: 3
397
+ };
398
+ var MISSION_RANK = {
399
+ scheduled: 0,
400
+ running: 1,
401
+ paused: 2,
402
+ waiting_approval: 2,
403
+ succeeded: 3,
404
+ aborted: 3,
405
+ cancelled: 3,
406
+ failed: 3
407
+ };
408
+ function maxStepStatus(current, next) {
409
+ if (STEP_RANK[next] <= STEP_RANK[current]) return current;
410
+ return next;
411
+ }
412
+ function maxMissionStatus(current, next) {
413
+ if (MISSION_RANK[next] <= MISSION_RANK[current]) return current;
414
+ return next;
415
+ }
416
+ function emptyMission(missionId) {
417
+ return {
418
+ missionId,
419
+ status: "scheduled",
420
+ steps: [],
421
+ spentUsd: 0,
422
+ lastEventAt: 0,
423
+ lastControlAt: 0
424
+ };
425
+ }
426
+ function stepStateFrom(step) {
427
+ return { id: step.id, intent: step.intent, kind: step.kind, status: step.status };
428
+ }
429
+ function applyMissionEvent(prev, event) {
430
+ const at = typeof event.at === "number" ? event.at : 0;
431
+ const base = prev ?? emptyMission(event.missionId);
432
+ const lastEventAt = Math.max(base.lastEventAt, at);
433
+ const lastControlAt = base.lastControlAt ?? 0;
434
+ switch (event.type) {
435
+ case "mission.created": {
436
+ const merged = event.steps.map((incoming) => {
437
+ const existing = base.steps.find((s) => s.id === incoming.id);
438
+ if (!existing) return stepStateFrom(incoming);
439
+ return {
440
+ ...existing,
441
+ intent: incoming.intent,
442
+ kind: incoming.kind,
443
+ status: maxStepStatus(existing.status, incoming.status)
444
+ };
445
+ });
446
+ for (const known of base.steps) {
447
+ if (!merged.some((s) => s.id === known.id)) merged.push(known);
448
+ }
449
+ return {
450
+ ...base,
451
+ title: event.title || base.title,
452
+ status: maxMissionStatus(base.status, event.status ?? base.status),
453
+ capUsd: event.budgetUsd ?? base.capUsd,
454
+ steps: merged,
455
+ lastEventAt
456
+ };
457
+ }
458
+ case "mission.started":
459
+ return { ...base, status: maxMissionStatus(base.status, "running"), lastEventAt };
460
+ case "step.started":
461
+ return {
462
+ ...base,
463
+ steps: upsertStep(base.steps, event.stepId, (step) => ({
464
+ ...step,
465
+ status: maxStepStatus(step.status, "running")
466
+ })),
467
+ lastEventAt
468
+ };
469
+ case "step.updated":
470
+ return {
471
+ ...base,
472
+ steps: upsertStep(base.steps, event.stepId, (step) => ({
473
+ ...step,
474
+ // A sublabel is a live counter ("7/15") — always take the latest; it
475
+ // does not move status.
476
+ ...event.sublabel !== void 0 ? { sublabel: event.sublabel } : {},
477
+ // agentActivity is a full snapshot, replaced wholesale. Guarded by
478
+ // `at` so a stale snapshot delivered late never erases newer rows;
479
+ // an equal-`at` replay rewrites identical content (idempotent).
480
+ ...event.agentActivity !== void 0 && at >= (step.agentActivityAt ?? 0) ? { agentActivity: event.agentActivity, agentActivityAt: at } : {}
481
+ })),
482
+ lastEventAt
483
+ };
484
+ case "step.completed":
485
+ return {
486
+ ...base,
487
+ steps: upsertStep(base.steps, event.stepId, (step) => ({
488
+ ...step,
489
+ status: maxStepStatus(step.status, event.ok ? "done" : "failed"),
490
+ ...event.reason !== void 0 ? { reason: event.reason } : {},
491
+ ...event.durationMs !== void 0 ? { durationMs: event.durationMs } : {}
492
+ })),
493
+ lastEventAt
494
+ };
495
+ case "cost.updated":
496
+ return {
497
+ ...base,
498
+ // spentUsd is cumulative and monotonically non-decreasing at the
499
+ // source; clamp so an out-of-order older value never lowers the
500
+ // displayed spend.
501
+ spentUsd: Math.max(base.spentUsd, event.spentUsd),
502
+ capUsd: event.capUsd ?? base.capUsd,
503
+ lastEventAt
504
+ };
505
+ case "mission.paused":
506
+ if (at <= lastControlAt) return { ...base, lastEventAt };
507
+ return {
508
+ ...base,
509
+ status: maxMissionStatus(base.status, "paused"),
510
+ ...event.reason !== void 0 ? { pauseReason: event.reason } : {},
511
+ lastEventAt,
512
+ lastControlAt: Math.max(lastControlAt, at)
513
+ };
514
+ case "mission.waiting_approval":
515
+ if (at <= lastControlAt) return { ...base, lastEventAt };
516
+ return {
517
+ ...base,
518
+ status: maxMissionStatus(base.status, "waiting_approval"),
519
+ ...event.reason !== void 0 ? { pauseReason: event.reason } : {},
520
+ lastEventAt,
521
+ lastControlAt: Math.max(lastControlAt, at)
522
+ };
523
+ case "mission.resumed":
524
+ if (at <= lastControlAt) return { ...base, lastEventAt };
525
+ return {
526
+ ...base,
527
+ status: isTerminalStreamStatus(base.status) ? base.status : "running",
528
+ pauseReason: void 0,
529
+ lastEventAt,
530
+ lastControlAt: Math.max(lastControlAt, at)
531
+ };
532
+ case "mission.plan.updated":
533
+ return {
534
+ ...base,
535
+ title: event.title || base.title,
536
+ capUsd: event.budgetUsd ?? base.capUsd,
537
+ steps: event.steps.map((incoming) => {
538
+ const existing = base.steps.find((s) => s.id === incoming.id);
539
+ if (!existing) return stepStateFrom(incoming);
540
+ return {
541
+ ...stepStateFrom(incoming),
542
+ status: maxStepStatus(existing.status, incoming.status),
543
+ ...existing.sublabel !== void 0 ? { sublabel: existing.sublabel } : {},
544
+ ...existing.reason !== void 0 ? { reason: existing.reason } : {},
545
+ ...existing.durationMs !== void 0 ? { durationMs: existing.durationMs } : {},
546
+ ...existing.agentActivity !== void 0 ? { agentActivity: existing.agentActivity } : {},
547
+ ...existing.agentActivityAt !== void 0 ? { agentActivityAt: existing.agentActivityAt } : {}
548
+ };
549
+ }),
550
+ lastEventAt
551
+ };
552
+ case "mission.completed":
553
+ return {
554
+ ...base,
555
+ status: maxMissionStatus(base.status, event.status ?? (event.ok ? "succeeded" : "failed")),
556
+ ...event.summary !== void 0 ? { summary: event.summary } : {},
557
+ lastEventAt
558
+ };
559
+ }
560
+ }
561
+ function isTerminalStreamStatus(status) {
562
+ return status === "succeeded" || status === "failed" || status === "aborted" || status === "cancelled";
563
+ }
564
+ function upsertStep(steps, stepId, patch) {
565
+ const index = steps.findIndex((s) => s.id === stepId);
566
+ const existing = index < 0 ? void 0 : steps[index];
567
+ if (!existing) {
568
+ const placeholder = {
569
+ id: stepId,
570
+ intent: "",
571
+ kind: "",
572
+ status: "pending"
573
+ };
574
+ return [...steps, patch(placeholder)];
575
+ }
576
+ const next = steps.slice();
577
+ next[index] = patch(existing);
578
+ return next;
579
+ }
580
+ function mergeMissionState(live, seed) {
581
+ if (!live) return seed;
582
+ const steps = [];
583
+ for (const seededStep of seed.steps) {
584
+ const current = live.steps.find((s) => s.id === seededStep.id);
585
+ if (!current) {
586
+ steps.push(seededStep);
587
+ continue;
588
+ }
589
+ steps.push({
590
+ ...seededStep,
591
+ intent: current.intent || seededStep.intent,
592
+ kind: current.kind || seededStep.kind,
593
+ status: maxStepStatus(current.status, seededStep.status),
594
+ ...current.sublabel !== void 0 ? { sublabel: current.sublabel } : seededStep.sublabel !== void 0 ? { sublabel: seededStep.sublabel } : {},
595
+ ...current.reason !== void 0 ? { reason: current.reason } : seededStep.reason !== void 0 ? { reason: seededStep.reason } : {},
596
+ ...current.durationMs !== void 0 ? { durationMs: current.durationMs } : seededStep.durationMs !== void 0 ? { durationMs: seededStep.durationMs } : {},
597
+ // The newer snapshot wins by its stamped `at`; an unstamped lane (loader
598
+ // seed copied from the settled artifact) only fills an empty live lane.
599
+ ...mergeActivity(current, seededStep)
600
+ });
601
+ }
602
+ for (const current of live.steps) {
603
+ if (seed.steps.some((step) => step.id === current.id)) continue;
604
+ if (hasStepProgressEvidence(current)) steps.push(current);
605
+ }
606
+ const status = mergeSeedMissionStatus(live.status, seed.status, live.lastControlAt ?? 0, seed.lastControlAt ?? 0);
607
+ return {
608
+ ...live,
609
+ title: live.title ?? seed.title,
610
+ status,
611
+ steps,
612
+ spentUsd: Math.max(live.spentUsd, seed.spentUsd),
613
+ capUsd: live.capUsd ?? seed.capUsd,
614
+ pauseReason: status === "running" || status === "scheduled" ? void 0 : seed.pauseReason ?? live.pauseReason,
615
+ summary: live.summary ?? seed.summary,
616
+ lastEventAt: Math.max(live.lastEventAt, seed.lastEventAt),
617
+ lastControlAt: Math.max(live.lastControlAt ?? 0, seed.lastControlAt ?? 0)
618
+ };
619
+ }
620
+ function hasStepProgressEvidence(step) {
621
+ return step.status !== "pending" || step.sublabel !== void 0 || step.reason !== void 0 || step.durationMs !== void 0 || step.agentActivity !== void 0;
622
+ }
623
+ function mergeActivity(live, seeded) {
624
+ const winner = (seeded.agentActivityAt ?? 0) > (live.agentActivityAt ?? 0) ? seeded : live;
625
+ if (winner.agentActivity === void 0) {
626
+ const fallback = winner === live ? seeded : live;
627
+ if (fallback.agentActivity === void 0) return {};
628
+ return {
629
+ agentActivity: fallback.agentActivity,
630
+ ...fallback.agentActivityAt !== void 0 ? { agentActivityAt: fallback.agentActivityAt } : {}
631
+ };
632
+ }
633
+ return {
634
+ agentActivity: winner.agentActivity,
635
+ ...winner.agentActivityAt !== void 0 ? { agentActivityAt: winner.agentActivityAt } : {}
636
+ };
637
+ }
638
+ function mergeSeedMissionStatus(liveStatus, seedStatus, liveControlAt, seedControlAt) {
639
+ if (isTerminalStreamStatus(liveStatus)) return liveStatus;
640
+ if (isTerminalStreamStatus(seedStatus)) return seedStatus;
641
+ if ((seedStatus === "paused" || seedStatus === "waiting_approval") && liveControlAt > seedControlAt) {
642
+ return liveStatus;
643
+ }
644
+ if (seedStatus === "running") return "running";
645
+ if (seedStatus === "scheduled" && liveStatus !== "scheduled") return liveStatus;
646
+ return maxMissionStatus(liveStatus, seedStatus);
647
+ }
648
+ function reduceMissionEvents(events, seed) {
649
+ const next = new Map(seed ?? []);
650
+ for (const event of events) {
651
+ next.set(event.missionId, applyMissionEvent(next.get(event.missionId), event));
652
+ }
653
+ return next;
654
+ }
655
+
656
+ // src/missions/engine.ts
657
+ var MissionConcurrencyError = class extends Error {
658
+ constructor(message) {
659
+ super(message);
660
+ this.name = "MissionConcurrencyError";
661
+ }
662
+ };
663
+ var RetryableStepError = class extends Error {
664
+ constructor(message) {
665
+ super(message);
666
+ this.name = "RetryableStepError";
667
+ }
668
+ };
669
+ var DEFAULT_EXTERNAL_ACTION_CAP = 5;
670
+ var DEFAULT_NON_FATAL_STEP_KINDS = ["optional", "best-effort"];
671
+ function stepGateProposalId(missionId, stepId) {
672
+ return `mission-step-gate:${missionId}:${stepId}`;
673
+ }
674
+ function budgetGateProposalId(missionId, stepId) {
675
+ return `mission-budget-gate:${missionId}:${stepId}`;
676
+ }
677
+ function volumeGateProposalId(missionId, stepId) {
678
+ return `mission-volume-gate:${missionId}:${stepId}`;
679
+ }
680
+ function safeEmit(sink, event) {
681
+ try {
682
+ sink.emit(event);
683
+ } catch {
684
+ }
685
+ }
686
+ function unblocked(resolution) {
687
+ return resolution === "approved" || resolution === "executed";
688
+ }
689
+ function terminalMissionEvent(missionId, status) {
690
+ const terminal = status === "succeeded" || status === "failed" || status === "aborted" || status === "cancelled" ? status : "failed";
691
+ return {
692
+ type: "mission.completed",
693
+ missionId,
694
+ at: Date.now(),
695
+ ok: terminal === "succeeded",
696
+ status: terminal,
697
+ ...terminal === "succeeded" ? {} : { summary: `Mission ${status}` }
698
+ };
699
+ }
700
+ function createMissionEngine(options) {
701
+ const { service, estimateStepCostUsd, gates } = options;
702
+ const sink = options.sink ?? noopEventSink;
703
+ const nonFatalStepKinds = new Set(options.nonFatalStepKinds ?? DEFAULT_NON_FATAL_STEP_KINDS);
704
+ const externalActionCap = gates?.externalActionCap ?? DEFAULT_EXTERNAL_ACTION_CAP;
705
+ function isFatalStepKind(kind) {
706
+ return !nonFatalStepKinds.has(kind);
707
+ }
708
+ function rejectStep(failure) {
709
+ if (failure.conflict) throw new MissionConcurrencyError(failure.error);
710
+ return { kind: "failed", error: failure.error, fatal: true };
711
+ }
712
+ function isStepCurrentOrFuture(mission, stepId) {
713
+ const index = mission.plan.findIndex((candidate) => candidate.id === stepId);
714
+ return index >= mission.cursor;
715
+ }
716
+ const recordCost = async (missionId, deltaUsd, ledgerDelta) => {
717
+ const recorded = await service.addCost(missionId, deltaUsd, ledgerDelta);
718
+ if (!recorded.succeeded) return recorded;
719
+ safeEmit(sink, {
720
+ type: "cost.updated",
721
+ missionId,
722
+ at: Date.now(),
723
+ spentUsd: recorded.value.spentUsd,
724
+ capUsd: recorded.value.budgetUsd
725
+ });
726
+ return recorded;
727
+ };
728
+ const pauseMission = async (missionId, reason) => {
729
+ const before = await service.getMission(missionId);
730
+ const paused = await service.pause(missionId, reason);
731
+ if (!paused.succeeded) return paused;
732
+ if (before?.status !== "paused") {
733
+ safeEmit(sink, {
734
+ type: "mission.paused",
735
+ missionId,
736
+ at: Date.now(),
737
+ reason: paused.value.pauseReason ?? reason
738
+ });
739
+ }
740
+ return paused;
741
+ };
742
+ const runStep = async (missionId, stepId, dispatch) => {
743
+ const mission = await service.getMission(missionId);
744
+ if (!mission) throw new MissionConcurrencyError(`Mission ${missionId} not found mid-run`);
745
+ const stepIndex = mission.plan.findIndex((candidate) => candidate.id === stepId);
746
+ const step = stepIndex < 0 ? void 0 : mission.plan[stepIndex];
747
+ if (!step) {
748
+ return { kind: "skipped-cursor", reason: `Step ${stepId} is no longer in mission plan` };
749
+ }
750
+ if (isMissionStopRequested(mission)) {
751
+ return { kind: "failed", error: mission.pauseReason ?? "Mission stop requested", fatal: true };
752
+ }
753
+ if (mission.status !== "running") {
754
+ return { kind: "skipped-cursor", reason: mission.pauseReason ?? `Mission is ${mission.status}` };
755
+ }
756
+ if (step.status === "done" && step.resultRef) {
757
+ if (stepIndex === mission.cursor) {
758
+ const reconciled = await service.advanceCursor(missionId);
759
+ if (!reconciled.succeeded) return rejectStep(reconciled);
760
+ }
761
+ safeEmit(sink, { type: "step.completed", missionId, at: Date.now(), stepId, ok: true });
762
+ return { kind: "done", resultRef: step.resultRef, cached: true };
763
+ }
764
+ if (stepIndex < mission.cursor) {
765
+ return { kind: "skipped-cursor", reason: `Step ${stepId} is behind cursor ${mission.cursor}` };
766
+ }
767
+ const startedAt = Date.now();
768
+ if (step.status !== "running") {
769
+ const running = await service.setStepStatus(missionId, stepId, "running");
770
+ if (!running.succeeded) return rejectStep(running);
771
+ }
772
+ safeEmit(sink, { type: "step.started", missionId, at: startedAt, stepId });
773
+ let dispatched;
774
+ try {
775
+ dispatched = await dispatch({ mission, step, stepIndex });
776
+ } catch (error) {
777
+ if (error instanceof RetryableStepError) throw error;
778
+ const latest = await service.getMission(missionId);
779
+ if (latest && !isMissionTerminal(latest.status) && !isMissionStopRequested(latest) && !isStepCurrentOrFuture(latest, stepId)) {
780
+ return { kind: "skipped-cursor", reason: `Step ${stepId} is no longer active` };
781
+ }
782
+ const message = error instanceof Error ? error.message : "Sandbox dispatch failed";
783
+ const failed = await service.setStepStatus(missionId, stepId, "failed", { error: message });
784
+ if (!failed.succeeded) return rejectStep(failed);
785
+ safeEmit(sink, {
786
+ type: "step.completed",
787
+ missionId,
788
+ at: Date.now(),
789
+ stepId,
790
+ ok: false,
791
+ reason: message,
792
+ durationMs: Date.now() - startedAt
793
+ });
794
+ return { kind: "failed", error: message, fatal: isFatalStepKind(step.kind) };
795
+ }
796
+ const afterDispatch = await service.getMission(missionId);
797
+ if (!afterDispatch) throw new MissionConcurrencyError(`Mission ${missionId} not found after dispatch`);
798
+ if (isMissionTerminal(afterDispatch.status) || isMissionStopRequested(afterDispatch)) {
799
+ return { kind: "failed", error: afterDispatch.pauseReason ?? "Mission stop requested", fatal: true };
800
+ }
801
+ if (afterDispatch.status !== "running") {
802
+ return { kind: "skipped-cursor", reason: afterDispatch.pauseReason ?? `Mission is ${afterDispatch.status}` };
803
+ }
804
+ if (!isStepCurrentOrFuture(afterDispatch, stepId)) {
805
+ return { kind: "skipped-cursor", reason: `Step ${stepId} is no longer active` };
806
+ }
807
+ if (dispatched.kind === "in_progress") {
808
+ if (dispatched.sublabel !== void 0) {
809
+ safeEmit(sink, { type: "step.updated", missionId, at: Date.now(), stepId, sublabel: dispatched.sublabel });
810
+ }
811
+ return {
812
+ kind: "in_progress",
813
+ sessionRef: dispatched.sessionRef,
814
+ pollAfterMs: dispatched.pollAfterMs,
815
+ ...dispatched.sublabel === void 0 ? {} : { sublabel: dispatched.sublabel }
816
+ };
817
+ }
818
+ if (dispatched.sublabel !== void 0) {
819
+ safeEmit(sink, { type: "step.updated", missionId, at: Date.now(), stepId, sublabel: dispatched.sublabel });
820
+ }
821
+ const deltaUsd = dispatched.cost?.deltaUsd ?? estimateStepCostUsd(step);
822
+ const chargeable = deltaUsd > 0 || Boolean(dispatched.cost?.ledgerDelta);
823
+ const spentBefore = afterDispatch.spentUsd;
824
+ const done = await service.setStepStatus(missionId, stepId, "done", {
825
+ resultRef: dispatched.resultRef,
826
+ ...dispatched.sublabel === void 0 ? {} : { sublabel: dispatched.sublabel },
827
+ ...chargeable ? {
828
+ cost: {
829
+ deltaUsd,
830
+ ledgerDelta: {
831
+ costUsd: deltaUsd,
832
+ llmCalls: 1,
833
+ ...dispatched.cost?.ledgerDelta ?? {}
834
+ }
835
+ }
836
+ } : {}
837
+ });
838
+ if (!done.succeeded) return rejectStep(done);
839
+ if (done.value.spentUsd !== spentBefore) {
840
+ safeEmit(sink, {
841
+ type: "cost.updated",
842
+ missionId,
843
+ at: Date.now(),
844
+ spentUsd: done.value.spentUsd,
845
+ capUsd: done.value.budgetUsd
846
+ });
847
+ }
848
+ safeEmit(sink, {
849
+ type: "step.completed",
850
+ missionId,
851
+ at: Date.now(),
852
+ stepId,
853
+ ok: true,
854
+ durationMs: Date.now() - startedAt
855
+ });
856
+ const advanced = await service.advanceCursor(missionId);
857
+ if (!advanced.succeeded) return rejectStep(advanced);
858
+ return { kind: "done", resultRef: dispatched.resultRef, cached: false };
859
+ };
860
+ async function parkForApproval(mission, step, reason) {
861
+ const waiting = await service.markWaitingApproval(mission.id, step.id);
862
+ if (!waiting.succeeded) {
863
+ if (waiting.conflict) throw new MissionConcurrencyError(waiting.error);
864
+ return { kind: "halted", status: mission.status, reason: waiting.error };
865
+ }
866
+ safeEmit(sink, {
867
+ type: "mission.waiting_approval",
868
+ missionId: mission.id,
869
+ at: Date.now(),
870
+ reason
871
+ });
872
+ safeEmit(sink, {
873
+ type: "mission.plan.updated",
874
+ missionId: mission.id,
875
+ at: Date.now(),
876
+ title: waiting.value.summary ?? "Mission",
877
+ steps: waiting.value.plan.map((candidate) => ({
878
+ id: candidate.id,
879
+ intent: candidate.intent,
880
+ kind: candidate.kind,
881
+ status: candidate.status
882
+ })),
883
+ budgetUsd: waiting.value.budgetUsd
884
+ });
885
+ return { kind: "halted", status: "waiting_approval", reason };
886
+ }
887
+ async function enforceBudget(mission, step) {
888
+ const capUsd = mission.budgetUsd;
889
+ if (capUsd === null) return { kind: "continue" };
890
+ const estimatedCostUsd = estimateStepCostUsd(step);
891
+ if (estimatedCostUsd <= 0) return { kind: "continue" };
892
+ if (Math.round((mission.spentUsd + estimatedCostUsd) * 100) <= Math.round(capUsd * 100)) {
893
+ return { kind: "continue" };
894
+ }
895
+ if (!gates) {
896
+ const reason = `Budget cap reached before step ${step.id}: $${mission.spentUsd.toFixed(2)} spent of $${capUsd.toFixed(2)}, next step estimated $${estimatedCostUsd.toFixed(2)}`;
897
+ const paused = await pauseMission(mission.id, reason);
898
+ if (!paused.succeeded) {
899
+ if (paused.conflict) throw new MissionConcurrencyError(paused.error);
900
+ return { kind: "halted", status: mission.status, reason: paused.error };
901
+ }
902
+ return { kind: "halted", status: paused.value.status, reason };
903
+ }
904
+ const proposalId = budgetGateProposalId(mission.id, step.id);
905
+ const resolution = await gates.approvals.findResolution(proposalId);
906
+ if (unblocked(resolution)) return { kind: "continue" };
907
+ if (resolution === null) {
908
+ await gates.approvals.createProposal({
909
+ id: proposalId,
910
+ missionId: mission.id,
911
+ stepId: step.id,
912
+ gate: "budget",
913
+ mission,
914
+ step,
915
+ budget: { spentUsd: mission.spentUsd, budgetUsd: capUsd, estimatedCostUsd }
916
+ });
917
+ }
918
+ return parkForApproval(mission, step, `Budget approval required for step ${step.id}`);
919
+ }
920
+ async function enforceVolumeCap(mission, step) {
921
+ if (!gates) return { kind: "continue" };
922
+ const overrideId = volumeGateProposalId(mission.id, step.id);
923
+ const override = await gates.approvals.findResolution(overrideId);
924
+ if (unblocked(override)) return { kind: "continue" };
925
+ const externalCount = await gates.approvals.countExternalActionProposals(mission.id);
926
+ if (externalCount < externalActionCap) return { kind: "continue" };
927
+ if (override === null) {
928
+ await gates.approvals.createProposal({
929
+ id: overrideId,
930
+ missionId: mission.id,
931
+ stepId: step.id,
932
+ gate: "volume",
933
+ mission,
934
+ step,
935
+ volume: { externalActionCount: externalCount, cap: externalActionCap }
936
+ });
937
+ }
938
+ return parkForApproval(mission, step, `External action cap approval required for step ${step.id}`);
939
+ }
940
+ async function enforceStepGate(mission, step) {
941
+ if (!gates) return { kind: "continue" };
942
+ const classification = gates.classifyStep(step);
943
+ if (!classification) return { kind: "continue" };
944
+ if (classification.externalAction) {
945
+ const volume = await enforceVolumeCap(mission, step);
946
+ if (volume.kind === "halted") return volume;
947
+ }
948
+ const proposalId = stepGateProposalId(mission.id, step.id);
949
+ const resolution = await gates.approvals.findResolution(proposalId);
950
+ if (unblocked(resolution)) return { kind: "continue" };
951
+ if (resolution === null) {
952
+ await gates.approvals.createProposal({
953
+ id: proposalId,
954
+ missionId: mission.id,
955
+ stepId: step.id,
956
+ gate: "step",
957
+ mission,
958
+ step,
959
+ classification
960
+ });
961
+ }
962
+ return parkForApproval(mission, step, `Approval required for step ${step.id}`);
963
+ }
964
+ const runPlan = async (missionId, runStepFn, planOptions = {}) => {
965
+ const mission = await service.getMission(missionId);
966
+ if (!mission) return { kind: "not-found" };
967
+ if (isMissionTerminal(mission.status)) {
968
+ safeEmit(sink, terminalMissionEvent(missionId, mission.status));
969
+ return { kind: "terminal", status: mission.status };
970
+ }
971
+ while (true) {
972
+ const currentMission = await service.getMission(missionId);
973
+ if (!currentMission) return { kind: "not-found" };
974
+ if (isMissionTerminal(currentMission.status)) {
975
+ safeEmit(sink, terminalMissionEvent(missionId, currentMission.status));
976
+ return { kind: "terminal", status: currentMission.status };
977
+ }
978
+ if (isMissionStopRequested(currentMission)) {
979
+ return {
980
+ kind: "halted",
981
+ status: currentMission.status,
982
+ reason: currentMission.pauseReason ?? "Mission stop requested"
983
+ };
984
+ }
985
+ if (currentMission.status !== "running") {
986
+ return {
987
+ kind: "halted",
988
+ status: currentMission.status,
989
+ reason: currentMission.pauseReason
990
+ };
991
+ }
992
+ const index = currentMission.cursor;
993
+ if (index >= currentMission.plan.length) break;
994
+ const step = currentMission.plan[index];
995
+ if (!step) break;
996
+ const haltReason = await planOptions.beforeStep?.(currentMission, step);
997
+ if (haltReason) {
998
+ const paused = await pauseMission(missionId, haltReason);
999
+ if (!paused.succeeded) throw new MissionConcurrencyError(paused.error);
1000
+ return { kind: "halted", status: paused.value.status, reason: paused.value.pauseReason ?? haltReason };
1001
+ }
1002
+ if (step.status !== "done") {
1003
+ const budget = await enforceBudget(currentMission, step);
1004
+ if (budget.kind === "halted") {
1005
+ return { kind: "halted", status: budget.status, reason: budget.reason };
1006
+ }
1007
+ const gate = await enforceStepGate(currentMission, step);
1008
+ if (gate.kind === "halted") {
1009
+ return { kind: "halted", status: gate.status, reason: gate.reason };
1010
+ }
1011
+ }
1012
+ const outcome = await runStepFn(step, index);
1013
+ if (outcome.kind === "failed" && outcome.fatal) {
1014
+ safeEmit(sink, {
1015
+ type: "mission.completed",
1016
+ missionId,
1017
+ at: Date.now(),
1018
+ ok: false,
1019
+ summary: `Step ${step.id} failed: ${outcome.error}`
1020
+ });
1021
+ return { kind: "failed", failedStepId: step.id, error: outcome.error };
1022
+ }
1023
+ if (outcome.kind === "failed" && !outcome.fatal) {
1024
+ const advanced = await service.advanceCursor(missionId);
1025
+ if (!advanced.succeeded) throw new MissionConcurrencyError(advanced.error);
1026
+ }
1027
+ if (outcome.kind === "in_progress") {
1028
+ return {
1029
+ kind: "in_progress",
1030
+ stepId: step.id,
1031
+ sessionRef: outcome.sessionRef,
1032
+ pollAfterMs: outcome.pollAfterMs,
1033
+ ...outcome.sublabel === void 0 ? {} : { sublabel: outcome.sublabel }
1034
+ };
1035
+ }
1036
+ }
1037
+ const finalMission = await service.getMission(missionId);
1038
+ const planLength = finalMission?.plan.length ?? 0;
1039
+ const summary = `Completed ${planLength} step${planLength === 1 ? "" : "s"}`;
1040
+ const completed = await service.complete(missionId, { ok: true, summary });
1041
+ if (!completed.succeeded) {
1042
+ const after = await service.getMission(missionId);
1043
+ if (after && isMissionTerminal(after.status)) {
1044
+ safeEmit(sink, terminalMissionEvent(missionId, after.status));
1045
+ return { kind: "terminal", status: after.status };
1046
+ }
1047
+ throw new MissionConcurrencyError(completed.error);
1048
+ }
1049
+ safeEmit(sink, { type: "mission.completed", missionId, at: Date.now(), ok: true, summary });
1050
+ return { kind: "completed", summary };
1051
+ };
1052
+ return { runStep, runPlan, recordCost, pauseMission };
1053
+ }
1054
+
1055
+ // src/missions/plan-parse.ts
1056
+ var DEFAULT_MISSION_STEP_KINDS = [
1057
+ "research",
1058
+ "generate",
1059
+ "analyze",
1060
+ "write",
1061
+ "best-effort"
1062
+ ];
1063
+ function parseMissionBlocks(fullContent, options = {}) {
1064
+ const kinds = new Set(options.kinds ?? DEFAULT_MISSION_STEP_KINDS);
1065
+ const missionRegex = /:::mission\s*\n([\s\S]*?)\n\s*:::/g;
1066
+ const missions = [];
1067
+ let match;
1068
+ while ((match = missionRegex.exec(fullContent)) !== null) {
1069
+ const body = match[1];
1070
+ if (body === void 0) continue;
1071
+ const parsed = parseMissionBody(body, kinds);
1072
+ if (parsed) missions.push(parsed);
1073
+ }
1074
+ return missions;
1075
+ }
1076
+ function parseMissionBody(body, kinds) {
1077
+ let title = null;
1078
+ const steps = [];
1079
+ for (const rawLine of body.split("\n")) {
1080
+ const line = rawLine.trim();
1081
+ if (!line) continue;
1082
+ const titleMatch = /^title\s*:\s*(.+)$/i.exec(line);
1083
+ if (titleMatch?.[1] !== void 0) {
1084
+ if (title === null) title = titleMatch[1].trim();
1085
+ continue;
1086
+ }
1087
+ const stepMatch = /^([A-Za-z0-9][A-Za-z0-9_-]*)\s*:\s*([A-Za-z-]+)\s*\|\s*(.+)$/.exec(line);
1088
+ if (!stepMatch) continue;
1089
+ const id = stepMatch[1]?.trim();
1090
+ const kind = stepMatch[2]?.trim().toLowerCase();
1091
+ const intent = stepMatch[3]?.trim();
1092
+ if (!id || !kind || !intent) continue;
1093
+ if (!kinds.has(kind)) continue;
1094
+ steps.push({ id, kind, intent });
1095
+ }
1096
+ if (!title || steps.length === 0) return null;
1097
+ return { title, steps };
1098
+ }
1099
+ function buildAgentMissionPlan(steps) {
1100
+ if (steps.length === 0) throw new Error("mission plan must have at least one step");
1101
+ const seen = /* @__PURE__ */ new Set();
1102
+ for (const step of steps) {
1103
+ if (seen.has(step.id)) {
1104
+ throw new Error(`duplicate mission step id "${step.id}" \u2014 step ids must be unique`);
1105
+ }
1106
+ seen.add(step.id);
1107
+ }
1108
+ return steps.map((step) => ({
1109
+ id: step.id,
1110
+ intent: step.intent,
1111
+ kind: step.kind,
1112
+ status: "pending",
1113
+ attempts: 0
1114
+ }));
1115
+ }
1116
+
1117
+ // src/missions/agent-activity.ts
1118
+ function stepAgentActivity(step) {
1119
+ const value = step.agentActivity;
1120
+ if (!Array.isArray(value)) return [];
1121
+ const items = [];
1122
+ for (const entry of value) {
1123
+ if (!entry || typeof entry !== "object") continue;
1124
+ const record = entry;
1125
+ if (typeof record.taskId !== "string" || typeof record.tool !== "string" || typeof record.status !== "string" || typeof record.detail !== "string" || typeof record.startedAt !== "string") {
1126
+ continue;
1127
+ }
1128
+ items.push({
1129
+ taskId: record.taskId,
1130
+ tool: record.tool,
1131
+ status: record.status,
1132
+ detail: record.detail,
1133
+ startedAt: record.startedAt,
1134
+ ...typeof record.costUsd === "number" && Number.isFinite(record.costUsd) ? { costUsd: record.costUsd } : {},
1135
+ ...typeof record.durationMs === "number" && Number.isFinite(record.durationMs) ? { durationMs: record.durationMs } : {},
1136
+ ...typeof record.iteration === "number" && Number.isFinite(record.iteration) ? { iteration: record.iteration } : {},
1137
+ ...typeof record.phase === "string" ? { phase: record.phase } : {},
1138
+ ...typeof record.traceId === "string" ? { traceId: record.traceId } : {},
1139
+ ...typeof record.spanId === "string" ? { spanId: record.spanId } : {}
1140
+ });
1141
+ }
1142
+ return items;
1143
+ }
24
1144
  export {
25
1145
  DEFAULT_MISSION_STEP_KINDS,
26
1146
  MISSION_CONTROL_CHANNEL_ID,