@duetso/agent 0.1.20

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 (148) hide show
  1. package/LICENSE +189 -0
  2. package/README.md +315 -0
  3. package/dist/package.json +84 -0
  4. package/dist/src/cli.d.ts +23 -0
  5. package/dist/src/cli.d.ts.map +1 -0
  6. package/dist/src/cli.js +754 -0
  7. package/dist/src/cli.js.map +1 -0
  8. package/dist/src/core/serializer.d.ts +3 -0
  9. package/dist/src/core/serializer.d.ts.map +1 -0
  10. package/dist/src/core/serializer.js +22 -0
  11. package/dist/src/core/serializer.js.map +1 -0
  12. package/dist/src/core/structured-output.d.ts +13 -0
  13. package/dist/src/core/structured-output.d.ts.map +1 -0
  14. package/dist/src/core/structured-output.js +41 -0
  15. package/dist/src/core/structured-output.js.map +1 -0
  16. package/dist/src/guardrails/firewall.d.ts +7 -0
  17. package/dist/src/guardrails/firewall.d.ts.map +1 -0
  18. package/dist/src/guardrails/firewall.js +31 -0
  19. package/dist/src/guardrails/firewall.js.map +1 -0
  20. package/dist/src/guardrails/pattern.d.ts +13 -0
  21. package/dist/src/guardrails/pattern.d.ts.map +1 -0
  22. package/dist/src/guardrails/pattern.js +70 -0
  23. package/dist/src/guardrails/pattern.js.map +1 -0
  24. package/dist/src/guardrails/semantic.d.ts +14 -0
  25. package/dist/src/guardrails/semantic.d.ts.map +1 -0
  26. package/dist/src/guardrails/semantic.js +47 -0
  27. package/dist/src/guardrails/semantic.js.map +1 -0
  28. package/dist/src/index.d.ts +20 -0
  29. package/dist/src/index.d.ts.map +1 -0
  30. package/dist/src/index.js +20 -0
  31. package/dist/src/index.js.map +1 -0
  32. package/dist/src/lib/compact-json.d.ts +11 -0
  33. package/dist/src/lib/compact-json.d.ts.map +1 -0
  34. package/dist/src/lib/compact-json.js +36 -0
  35. package/dist/src/lib/compact-json.js.map +1 -0
  36. package/dist/src/lib/xml.d.ts +3 -0
  37. package/dist/src/lib/xml.d.ts.map +1 -0
  38. package/dist/src/lib/xml.js +9 -0
  39. package/dist/src/lib/xml.js.map +1 -0
  40. package/dist/src/memory/observation-groups.d.ts +15 -0
  41. package/dist/src/memory/observation-groups.d.ts.map +1 -0
  42. package/dist/src/memory/observation-groups.js +159 -0
  43. package/dist/src/memory/observation-groups.js.map +1 -0
  44. package/dist/src/memory/observational-prompts.d.ts +27 -0
  45. package/dist/src/memory/observational-prompts.d.ts.map +1 -0
  46. package/dist/src/memory/observational-prompts.js +237 -0
  47. package/dist/src/memory/observational-prompts.js.map +1 -0
  48. package/dist/src/memory/observational.d.ts +63 -0
  49. package/dist/src/memory/observational.d.ts.map +1 -0
  50. package/dist/src/memory/observational.js +605 -0
  51. package/dist/src/memory/observational.js.map +1 -0
  52. package/dist/src/memory/storage.d.ts +3 -0
  53. package/dist/src/memory/storage.d.ts.map +1 -0
  54. package/dist/src/memory/storage.js +127 -0
  55. package/dist/src/memory/storage.js.map +1 -0
  56. package/dist/src/memory/store.d.ts +13 -0
  57. package/dist/src/memory/store.d.ts.map +1 -0
  58. package/dist/src/memory/store.js +106 -0
  59. package/dist/src/memory/store.js.map +1 -0
  60. package/dist/src/model-resolution/duet-gateway.d.ts +35 -0
  61. package/dist/src/model-resolution/duet-gateway.d.ts.map +1 -0
  62. package/dist/src/model-resolution/duet-gateway.js +56 -0
  63. package/dist/src/model-resolution/duet-gateway.js.map +1 -0
  64. package/dist/src/model-resolution/index.d.ts +31 -0
  65. package/dist/src/model-resolution/index.d.ts.map +1 -0
  66. package/dist/src/model-resolution/index.js +129 -0
  67. package/dist/src/model-resolution/index.js.map +1 -0
  68. package/dist/src/session/session-manager.d.ts +45 -0
  69. package/dist/src/session/session-manager.d.ts.map +1 -0
  70. package/dist/src/session/session-manager.js +94 -0
  71. package/dist/src/session/session-manager.js.map +1 -0
  72. package/dist/src/session/session.d.ts +113 -0
  73. package/dist/src/session/session.d.ts.map +1 -0
  74. package/dist/src/session/session.js +308 -0
  75. package/dist/src/session/session.js.map +1 -0
  76. package/dist/src/tui/app.d.ts +60 -0
  77. package/dist/src/tui/app.d.ts.map +1 -0
  78. package/dist/src/tui/app.js +742 -0
  79. package/dist/src/tui/app.js.map +1 -0
  80. package/dist/src/turn-runner/agent-events.d.ts +5 -0
  81. package/dist/src/turn-runner/agent-events.d.ts.map +1 -0
  82. package/dist/src/turn-runner/agent-events.js +59 -0
  83. package/dist/src/turn-runner/agent-events.js.map +1 -0
  84. package/dist/src/turn-runner/prompts.d.ts +13 -0
  85. package/dist/src/turn-runner/prompts.d.ts.map +1 -0
  86. package/dist/src/turn-runner/prompts.js +79 -0
  87. package/dist/src/turn-runner/prompts.js.map +1 -0
  88. package/dist/src/turn-runner/shell-state-handle.d.ts +32 -0
  89. package/dist/src/turn-runner/shell-state-handle.d.ts.map +1 -0
  90. package/dist/src/turn-runner/shell-state-handle.js +168 -0
  91. package/dist/src/turn-runner/shell-state-handle.js.map +1 -0
  92. package/dist/src/turn-runner/skill-context.d.ts +26 -0
  93. package/dist/src/turn-runner/skill-context.d.ts.map +1 -0
  94. package/dist/src/turn-runner/skill-context.js +110 -0
  95. package/dist/src/turn-runner/skill-context.js.map +1 -0
  96. package/dist/src/turn-runner/skills.d.ts +35 -0
  97. package/dist/src/turn-runner/skills.d.ts.map +1 -0
  98. package/dist/src/turn-runner/skills.js +130 -0
  99. package/dist/src/turn-runner/skills.js.map +1 -0
  100. package/dist/src/turn-runner/state-machine-controller.d.ts +90 -0
  101. package/dist/src/turn-runner/state-machine-controller.d.ts.map +1 -0
  102. package/dist/src/turn-runner/state-machine-controller.js +289 -0
  103. package/dist/src/turn-runner/state-machine-controller.js.map +1 -0
  104. package/dist/src/turn-runner/state-machine-session.d.ts +27 -0
  105. package/dist/src/turn-runner/state-machine-session.d.ts.map +1 -0
  106. package/dist/src/turn-runner/state-machine-session.js +189 -0
  107. package/dist/src/turn-runner/state-machine-session.js.map +1 -0
  108. package/dist/src/turn-runner/tools.d.ts +193 -0
  109. package/dist/src/turn-runner/tools.d.ts.map +1 -0
  110. package/dist/src/turn-runner/tools.js +509 -0
  111. package/dist/src/turn-runner/tools.js.map +1 -0
  112. package/dist/src/turn-runner/turn-runner.d.ts +160 -0
  113. package/dist/src/turn-runner/turn-runner.d.ts.map +1 -0
  114. package/dist/src/turn-runner/turn-runner.js +907 -0
  115. package/dist/src/turn-runner/turn-runner.js.map +1 -0
  116. package/dist/src/turn-runner/turn-state.d.ts +6 -0
  117. package/dist/src/turn-runner/turn-state.d.ts.map +1 -0
  118. package/dist/src/turn-runner/turn-state.js +32 -0
  119. package/dist/src/turn-runner/turn-state.js.map +1 -0
  120. package/dist/src/turn-runner/usage-accounting.d.ts +7 -0
  121. package/dist/src/turn-runner/usage-accounting.d.ts.map +1 -0
  122. package/dist/src/turn-runner/usage-accounting.js +49 -0
  123. package/dist/src/turn-runner/usage-accounting.js.map +1 -0
  124. package/dist/src/types/agent.d.ts +15 -0
  125. package/dist/src/types/agent.d.ts.map +1 -0
  126. package/dist/src/types/agent.js +2 -0
  127. package/dist/src/types/agent.js.map +1 -0
  128. package/dist/src/types/config.d.ts +37 -0
  129. package/dist/src/types/config.d.ts.map +1 -0
  130. package/dist/src/types/config.js +2 -0
  131. package/dist/src/types/config.js.map +1 -0
  132. package/dist/src/types/guardrails.d.ts +34 -0
  133. package/dist/src/types/guardrails.d.ts.map +1 -0
  134. package/dist/src/types/guardrails.js +2 -0
  135. package/dist/src/types/guardrails.js.map +1 -0
  136. package/dist/src/types/memory.d.ts +151 -0
  137. package/dist/src/types/memory.d.ts.map +1 -0
  138. package/dist/src/types/memory.js +2 -0
  139. package/dist/src/types/memory.js.map +1 -0
  140. package/dist/src/types/protocol.d.ts +426 -0
  141. package/dist/src/types/protocol.d.ts.map +1 -0
  142. package/dist/src/types/protocol.js +2 -0
  143. package/dist/src/types/protocol.js.map +1 -0
  144. package/dist/src/types/state-machine.d.ts +344 -0
  145. package/dist/src/types/state-machine.d.ts.map +1 -0
  146. package/dist/src/types/state-machine.js +2 -0
  147. package/dist/src/types/state-machine.js.map +1 -0
  148. package/package.json +84 -0
@@ -0,0 +1,907 @@
1
+ import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { getEnvApiKey } from "@earendil-works/pi-ai";
3
+ import dedent from "dedent";
4
+ import { assistantText } from "../core/serializer.js";
5
+ import { toXML } from "../lib/xml.js";
6
+ import { createObservationalMemoryTransform } from "../memory/observational.js";
7
+ import { loadStoredMemory } from "../memory/storage.js";
8
+ import { MemoryStore } from "../memory/store.js";
9
+ import { DEFAULT_CLI_MEMORY_MODEL, DEFAULT_CLI_MODEL, resolveModelName, } from "../model-resolution/index.js";
10
+ import { agentEventToTurnEvents, agentMessageText } from "./agent-events.js";
11
+ import { createStateMachineSystemPromptLayer } from "./prompts.js";
12
+ import { createDefaultTurnRunnerTools, createTurnRunnerTools, isTurnRunnerControlResult, } from "./tools.js";
13
+ import { SkillContext } from "./skill-context.js";
14
+ import { currentScheduledState, isWaitingOnScheduledState } from "./state-machine-session.js";
15
+ import { StateMachineController, } from "./state-machine-controller.js";
16
+ import { completeTurn, copyOptionalArray, createInitialTurnState } from "./turn-state.js";
17
+ import { addUsage, usageFromMessages } from "./usage-accounting.js";
18
+ export class TurnRunner {
19
+ config;
20
+ eventHandlers = new Set();
21
+ /** In-memory observation store used by context transforms during agent turns. */
22
+ memory = new MemoryStore();
23
+ /** Stops memory persistence subscriptions/databases when the runner is disposed. */
24
+ memoryDispose;
25
+ /**
26
+ * Session-scoped parent pi agent. It is created once during start() so model,
27
+ * tools, and system prompt shape stay stable for prompt caching while the
28
+ * transcript grows across every pi-agent turn in this duet-agent session.
29
+ */
30
+ parentAgent;
31
+ /** True only while the parent pi agent is actively producing the public terminal event. */
32
+ parentAgentRunning = false;
33
+ /** Last turn-runner control tool result observed from the parent agent. */
34
+ parentControlResult = { type: "none" };
35
+ /** Runtime owner for state-machine progress and active state work. */
36
+ stateMachineController;
37
+ /** Parent prompt started by a steer while state-machine work is driving the turn. */
38
+ activeStateWorkPrompt;
39
+ /** Terminal event prepared by `interrupt()` and returned when active work unwinds. */
40
+ interruptedTerminal;
41
+ /**
42
+ * Active work-chain promise. Callers may call turn() repeatedly while this is
43
+ * set; those commands are folded into the same duet-agent turn. A duet-agent
44
+ * turn may contain multiple parent pi-agent turns and multiple state-machine
45
+ * transitions, but it emits one public terminal event for the whole chain.
46
+ */
47
+ activeTurnPromise;
48
+ /** Latest runner-owned state, hydrated by start() and advanced by terminal events. */
49
+ state;
50
+ /** True after `start()` has emitted the initial `turn_started` event. */
51
+ started = false;
52
+ /** Aggregates model usage across parent agents, state agents, and memory work for one turn chain. */
53
+ turnUsage;
54
+ /** Ensures persisted memory hydrates once before the first turn that needs it. */
55
+ memoryLoaded = false;
56
+ skillContext;
57
+ constructor(config) {
58
+ this.config = config;
59
+ this.skillContext = new SkillContext(config);
60
+ this.stateMachineController = new StateMachineController({
61
+ cwd: config.cwd ?? process.cwd(),
62
+ createStateAgent: (input) => this.createStateAgentHandle(input),
63
+ });
64
+ }
65
+ async dispose() {
66
+ this.parentAgent?.clearAllQueues();
67
+ this.activeStateWorkPrompt = undefined;
68
+ this.setQueuedCommands([]);
69
+ this.clearFollowUpQueue();
70
+ await this.memoryDispose?.();
71
+ this.memoryDispose = undefined;
72
+ }
73
+ subscribe(handler) {
74
+ this.eventHandlers.add(handler);
75
+ return () => {
76
+ this.eventHandlers.delete(handler);
77
+ };
78
+ }
79
+ editFollowUpQueue(command) {
80
+ this.requireStarted();
81
+ this.replaceFollowUpQueue(command.prompts);
82
+ }
83
+ /**
84
+ * Set up a session before any turn runs. Loads memory and skills, then
85
+ * emits `turn_started` with the initial state (a fresh state, or the
86
+ * resumed state when `command.state` is provided). No agent work runs.
87
+ *
88
+ * Callers (CLI/TUI/session managers) call this once on launch so the user
89
+ * sees available skills before typing the first prompt.
90
+ */
91
+ async start(command) {
92
+ await this.ensureMemoryLoaded();
93
+ await this.ensureSkillsLoaded();
94
+ const mode = command.mode ?? this.config.mode ?? "auto";
95
+ const startOptions = command.options;
96
+ const state = command.state
97
+ ? {
98
+ ...command.state,
99
+ options: this.resolveTurnOptions(startOptions, command.state.options),
100
+ }
101
+ : createInitialTurnState(mode, this.resolveTurnOptions(startOptions));
102
+ this.stateMachineController.hydrate(state.stateMachine);
103
+ this.setState(state);
104
+ this.initializeParentAgent();
105
+ this.started = true;
106
+ const hydratedState = this.requireRunnerState();
107
+ this.emit({ type: "turn_started", state: hydratedState });
108
+ return hydratedState;
109
+ }
110
+ async turn(command) {
111
+ this.requireStarted();
112
+ await this.ensureMemoryLoaded();
113
+ await this.ensureSkillsLoaded();
114
+ if (this.activeTurnPromise) {
115
+ // turn() is the concurrency boundary: repeated calls extend or queue
116
+ // behind the active chain instead of creating a separate parent transcript.
117
+ this.handleCommandDuringActiveTurn(command);
118
+ return this.activeTurnPromise;
119
+ }
120
+ const activeTurnPromise = this.runTurnChain(command);
121
+ this.activeTurnPromise = activeTurnPromise;
122
+ try {
123
+ return await activeTurnPromise;
124
+ }
125
+ finally {
126
+ if (this.activeTurnPromise === activeTurnPromise) {
127
+ this.activeTurnPromise = undefined;
128
+ }
129
+ }
130
+ }
131
+ async runTurnChain(command) {
132
+ this.turnUsage = undefined;
133
+ try {
134
+ let terminal;
135
+ terminal = await this.executeTurnCommand(command);
136
+ terminal = await this.drainQueuedTurnCommands(terminal);
137
+ terminal = { ...terminal, state: this.snapshotState(terminal.state) };
138
+ if (this.turnUsage) {
139
+ terminal = { ...terminal, usage: this.turnUsage };
140
+ }
141
+ this.setState(terminal.state);
142
+ this.emit(terminal);
143
+ return terminal;
144
+ }
145
+ finally {
146
+ this.turnUsage = undefined;
147
+ }
148
+ }
149
+ async executeTurnCommand(command) {
150
+ switch (command.type) {
151
+ case "prompt":
152
+ return this.prompt(command);
153
+ case "answer":
154
+ return this.answer(command);
155
+ case "wake":
156
+ return this.wake();
157
+ }
158
+ }
159
+ handleCommandDuringActiveTurn(command) {
160
+ if ((command.type === "prompt" || command.type === "answer") && this.parentAgentRunning) {
161
+ // The parent pi-agent is currently driving the public terminal event, so
162
+ // user input can go straight to pi using pi's native steer/follow-up queues.
163
+ this.sendCommandToAgent(this.requireParentAgent(), command);
164
+ return;
165
+ }
166
+ if (command.type === "prompt" || command.type === "answer") {
167
+ const message = this.commandToUserMessage(command);
168
+ if (this.stateMachineController.hasActiveWork()) {
169
+ if (command.behavior === "follow_up") {
170
+ // State-machine work is driving the terminal event. Follow-ups are
171
+ // transition context, so replay them before the next state decision.
172
+ this.enqueueTurnCommand(command);
173
+ return;
174
+ }
175
+ this.startParentPromptDuringActiveStateWork(message, command);
176
+ return;
177
+ }
178
+ }
179
+ this.enqueueTurnCommand(command);
180
+ }
181
+ sendCommandToAgent(agent, command) {
182
+ const message = this.commandToUserMessage(command);
183
+ const agentMessage = { role: "user", content: message, timestamp: Date.now() };
184
+ if (command.behavior === "steer") {
185
+ agent.steer(agentMessage);
186
+ }
187
+ else {
188
+ this.appendFollowUpPrompt(message);
189
+ agent.followUp(agentMessage);
190
+ }
191
+ }
192
+ startParentPromptDuringActiveStateWork(message, command) {
193
+ const prompt = this.runParentPromptDuringActiveStateWork(message, command);
194
+ this.activeStateWorkPrompt = prompt;
195
+ void prompt.finally(() => {
196
+ if (this.activeStateWorkPrompt === prompt)
197
+ this.activeStateWorkPrompt = undefined;
198
+ });
199
+ }
200
+ async runParentPromptDuringActiveStateWork(message, command) {
201
+ const prompt = command.behavior === "steer"
202
+ ? dedent `
203
+ <system-reminder>
204
+ The user sent this as a steer message while state-machine work is running.
205
+ If the state-machine should change course, call select_state_machine_state to restart the current state with updated input or choose a different state.
206
+ </system-reminder>
207
+
208
+ ${message}
209
+ `
210
+ : message;
211
+ const terminal = await this.prompt({
212
+ type: "prompt",
213
+ message: prompt,
214
+ behavior: command.behavior,
215
+ });
216
+ this.setState(terminal.state);
217
+ return terminal;
218
+ }
219
+ async drainQueuedTurnCommands(terminal) {
220
+ let latest = terminal;
221
+ while (this.getQueuedCommands().length > 0) {
222
+ if (latest.type === "sleep" &&
223
+ this.getQueuedCommands().every((command) => this.isFollowUpQueueCommand(command))) {
224
+ return latest;
225
+ }
226
+ if (latest.type === "interrupted" ||
227
+ (latest.type === "complete" && latest.status === "failed")) {
228
+ this.setQueuedCommands([]);
229
+ this.clearFollowUpQueue();
230
+ return latest;
231
+ }
232
+ const queued = this.shiftQueuedCommand();
233
+ if (!queued)
234
+ break;
235
+ this.removeQueuedFollowUpPrompt(queued);
236
+ this.setState({
237
+ ...latest.state,
238
+ followUpQueue: this.getFollowUpQueue(),
239
+ queuedCommands: this.getQueuedCommands(),
240
+ });
241
+ latest = await this.executeTurnCommand(queued);
242
+ }
243
+ return latest;
244
+ }
245
+ commandToUserMessage(command) {
246
+ if (command.type === "prompt") {
247
+ return this.skillContext.resolveSlashSkillPrompt(command.message);
248
+ }
249
+ return dedent `
250
+ Here are my answers to your questions.
251
+
252
+ ${toXML([{ questions: command.questions }, { answers: command.answers }])}
253
+ `;
254
+ }
255
+ enqueueTurnCommand(command) {
256
+ if ((command.type === "prompt" || command.type === "answer") &&
257
+ command.behavior === "follow_up") {
258
+ this.appendFollowUpPrompt(this.commandToUserMessage(command));
259
+ }
260
+ this.setQueuedCommands([...this.getQueuedCommands(), command]);
261
+ }
262
+ replaceFollowUpQueue(prompts) {
263
+ this.setFollowUpQueue(prompts);
264
+ this.parentAgent?.clearFollowUpQueue();
265
+ for (const prompt of this.getFollowUpQueue()) {
266
+ this.parentAgent?.followUp({
267
+ role: "user",
268
+ content: prompt,
269
+ timestamp: Date.now(),
270
+ });
271
+ }
272
+ this.replaceQueuedFollowUpCommands(prompts);
273
+ this.emitFollowUpQueue();
274
+ }
275
+ replaceQueuedFollowUpCommands(prompts) {
276
+ this.removeQueuedFollowUpCommands();
277
+ if (!this.state || this.parentAgentRunning)
278
+ return;
279
+ this.setQueuedCommands([
280
+ ...this.getQueuedCommands(),
281
+ ...prompts.map((prompt) => ({
282
+ type: "prompt",
283
+ message: prompt,
284
+ behavior: "follow_up",
285
+ })),
286
+ ]);
287
+ }
288
+ removeQueuedFollowUpCommands() {
289
+ this.setQueuedCommands(this.getQueuedCommands().filter((command) => !this.isFollowUpQueueCommand(command)));
290
+ }
291
+ isFollowUpQueueCommand(command) {
292
+ return ((command.type === "prompt" || command.type === "answer") && command.behavior === "follow_up");
293
+ }
294
+ appendFollowUpPrompt(prompt) {
295
+ this.setFollowUpQueue([...this.getFollowUpQueue(), prompt]);
296
+ this.emitFollowUpQueue();
297
+ }
298
+ removeQueuedFollowUpPrompt(command) {
299
+ if (!this.isFollowUpQueueCommand(command))
300
+ return;
301
+ this.removeFollowUpPrompt(this.commandToUserMessage(command));
302
+ }
303
+ removeFollowUpPrompt(prompt) {
304
+ const prompts = this.getFollowUpQueue();
305
+ const index = prompts.indexOf(prompt);
306
+ if (index === -1)
307
+ return;
308
+ this.setFollowUpQueue([...prompts.slice(0, index), ...prompts.slice(index + 1)]);
309
+ this.emitFollowUpQueue();
310
+ }
311
+ clearFollowUpQueue() {
312
+ if (this.getFollowUpQueue().length === 0)
313
+ return;
314
+ this.setFollowUpQueue([]);
315
+ this.emitFollowUpQueue();
316
+ }
317
+ emitFollowUpQueue() {
318
+ this.emit({ type: "follow_up_queue", prompts: this.getFollowUpQueue() });
319
+ }
320
+ interrupt(_command) {
321
+ this.requireStarted();
322
+ if (!this.state)
323
+ return;
324
+ const hadActiveControllerWork = this.stateMachineController.hasActiveWork();
325
+ this.stateMachineController.interrupt("Interrupted");
326
+ const interruptedState = this.snapshotState(this.state);
327
+ const terminal = {
328
+ type: "interrupted",
329
+ state: {
330
+ ...interruptedState,
331
+ status: "interrupted",
332
+ agent: { ...interruptedState.agent, status: "cancelled" },
333
+ },
334
+ };
335
+ this.setState(terminal.state);
336
+ if (this.parentAgentRunning || hadActiveControllerWork) {
337
+ // The active turn emits this terminal event after agent.prompt() unwinds.
338
+ // interrupt() only aborts out-of-band; it does not own turn completion.
339
+ this.interruptedTerminal = terminal;
340
+ }
341
+ this.parentAgent?.abort();
342
+ this.parentAgent?.clearAllQueues();
343
+ this.activeStateWorkPrompt = undefined;
344
+ this.setQueuedCommands([]);
345
+ this.clearFollowUpQueue();
346
+ this.parentAgentRunning = false;
347
+ }
348
+ emit(event) {
349
+ for (const handler of this.eventHandlers) {
350
+ handler(event);
351
+ }
352
+ }
353
+ consumeInterruptedTerminal() {
354
+ const terminal = this.interruptedTerminal;
355
+ this.interruptedTerminal = undefined;
356
+ return terminal;
357
+ }
358
+ async prompt(command) {
359
+ const originalState = this.requireRunnerState();
360
+ const state = { ...originalState, status: "running" };
361
+ const prompt = this.skillContext.resolveSlashSkillPrompt(command.message);
362
+ let terminal;
363
+ if (state.mode === "agent") {
364
+ terminal = await this.runAgentMode(state, prompt);
365
+ }
366
+ else {
367
+ terminal = await this.runTurnRunnerAgentWithStateMachineTools({
368
+ state,
369
+ prompt,
370
+ mode: state.mode,
371
+ });
372
+ }
373
+ return this.restoreSleepAfterPromptIfNeeded(originalState, terminal);
374
+ }
375
+ async answer(command) {
376
+ const message = this.commandToUserMessage(command);
377
+ return this.prompt({
378
+ type: "prompt",
379
+ message,
380
+ behavior: command.behavior,
381
+ });
382
+ }
383
+ async wake() {
384
+ const originalState = this.requireRunnerState();
385
+ const state = { ...originalState, status: "running" };
386
+ if (originalState.status === "sleeping") {
387
+ // Sleeping scheduled states already ended the previous duet-agent turn.
388
+ // Wake starts a new state-machine-driven turn; normal prompts while
389
+ // sleeping instead start parent-driven turns.
390
+ const result = await this.stateMachineController.wake();
391
+ if (result)
392
+ return this.driveStateMachineResult(result, state);
393
+ }
394
+ return {
395
+ type: "complete",
396
+ status: "completed",
397
+ state: originalState,
398
+ result: "Nothing to wake.",
399
+ };
400
+ }
401
+ restoreSleepAfterPromptIfNeeded(originalState, terminal) {
402
+ if (originalState.status !== "sleeping" ||
403
+ terminal.type !== "complete" ||
404
+ !isWaitingOnScheduledState(terminal.state.stateMachine)) {
405
+ return terminal;
406
+ }
407
+ if (terminal.status === "failed") {
408
+ this.emit({
409
+ type: "system",
410
+ level: "error",
411
+ message: terminal.error ?? terminal.result ?? "Prompt failed while waiting.",
412
+ });
413
+ }
414
+ const state = currentScheduledState(terminal.state.stateMachine);
415
+ const progress = state ? terminal.state.stateMachine?.progress?.states[state.name] : undefined;
416
+ const wakeAt = progress?.nextWakeAt ??
417
+ (state?.kind === "poll" ? Date.now() + state.intervalMs : (state?.wakeAt ?? Date.now()));
418
+ return {
419
+ type: "sleep",
420
+ wakeAt,
421
+ state: { ...terminal.state, status: "sleeping" },
422
+ };
423
+ }
424
+ controllerResultToTerminal(result, baseState = this.requireRunnerState()) {
425
+ const state = this.snapshotState(baseState);
426
+ switch (result.type) {
427
+ case "state_completed":
428
+ return completeTurn(state, "completed", JSON.stringify(result.output ?? null));
429
+ case "ask":
430
+ return {
431
+ type: "ask",
432
+ questions: result.questions,
433
+ state: { ...state, status: "waiting_for_human" },
434
+ };
435
+ case "sleep":
436
+ return { type: "sleep", wakeAt: result.wakeAt, state: { ...state, status: "sleeping" } };
437
+ case "interrupted":
438
+ return { type: "interrupted", state: { ...state, status: "interrupted" } };
439
+ case "terminal":
440
+ return completeTurn(state, result.status, result.result, result.error);
441
+ }
442
+ }
443
+ async driveStateMachineResult(result, baseState = this.requireRunnerState()) {
444
+ let next = result;
445
+ let state = baseState;
446
+ // The controller only executes states. TurnRunner owns the continuation
447
+ // loop so queued follow-ups always update the parent transcript before the
448
+ // parent chooses the next state.
449
+ while (next.type === "state_completed") {
450
+ state = this.requireRunnerState();
451
+ const queued = await this.drainQueuedTurnCommands({
452
+ type: "complete",
453
+ status: "completed",
454
+ state: this.snapshotState({ ...state, status: "running" }),
455
+ });
456
+ if (queued.type !== "complete" || queued.status !== "completed")
457
+ return queued;
458
+ this.setState(queued.state);
459
+ state = queued.state;
460
+ next = await this.selectNextStateAfterCompletion(next.stateName, next.output);
461
+ }
462
+ if (next.type === "interrupted" && this.activeStateWorkPrompt) {
463
+ return this.activeStateWorkPrompt;
464
+ }
465
+ return this.controllerResultToTerminal(next, state);
466
+ }
467
+ async selectNextStateAfterCompletion(stateName, output) {
468
+ for (let attempt = 1; attempt <= 3; attempt++) {
469
+ const retryInstruction = attempt === 1
470
+ ? undefined
471
+ : `This is retry ${attempt} of 3. You did not call select_state_machine_state last time. You must call select_state_machine_state now.`;
472
+ const turnState = this.snapshotState({ ...this.requireRunnerState(), status: "running" });
473
+ const workerResult = await this.runAgentWorkerWithUsage({
474
+ state: turnState,
475
+ prompt: dedent `
476
+ The state "${stateName}" finished.
477
+
478
+ ${toXML({
479
+ state_completed: {
480
+ output: output ?? null,
481
+ },
482
+ })}
483
+
484
+ ${retryInstruction ?? ""}
485
+
486
+ You must call the select_state_machine_state tool to choose the next state, terminal state, or failure outcome.
487
+ Do not answer normally. Do not return text instead of calling the tool.
488
+ `,
489
+ ...this.createTools(turnState.mode),
490
+ });
491
+ this.setState(workerResult.terminal.state);
492
+ const result = await this.controllerResultFromWorkerResult(workerResult, workerResult.terminal.state);
493
+ if (!result) {
494
+ continue;
495
+ }
496
+ return result;
497
+ }
498
+ return {
499
+ type: "terminal",
500
+ status: "failed",
501
+ error: "State completed, but the runner did not call select_state_machine_state.",
502
+ };
503
+ }
504
+ createStateAgentHandle(input) {
505
+ let control = { type: "none" };
506
+ const state = {
507
+ status: "running",
508
+ mode: "agent",
509
+ options: this.requireRunnerState().options,
510
+ agent: { status: "running", messages: [] },
511
+ };
512
+ const agent = this.createAgent({
513
+ state,
514
+ appendSystemPrompt: input.state.systemPrompt,
515
+ skills: this.skillContext.resolveStateAgentSkills(input.state),
516
+ ...this.createTools("agent"),
517
+ }, (result) => {
518
+ control = result;
519
+ });
520
+ let unsubscribe;
521
+ const finish = () => {
522
+ const usage = usageFromMessages(agent.state.messages);
523
+ this.recordUsage(usage);
524
+ if (control.type === "ask_user_question") {
525
+ return { type: "ask", questions: control.questions };
526
+ }
527
+ if (agent.state.errorMessage) {
528
+ return { type: "failed", error: agent.state.errorMessage };
529
+ }
530
+ return { type: "complete", result: assistantText(agent.state.messages) };
531
+ };
532
+ return {
533
+ prompt: async () => {
534
+ unsubscribe = agent.subscribe((event) => this.emitAgentEvent(event));
535
+ try {
536
+ await agent.prompt(input.prompt);
537
+ return finish();
538
+ }
539
+ catch (error) {
540
+ if (this.consumeInterruptedTerminal())
541
+ return { type: "interrupted" };
542
+ if (error instanceof Error)
543
+ return { type: "failed", error: error.message };
544
+ return { type: "failed", error: String(error) };
545
+ }
546
+ finally {
547
+ unsubscribe?.();
548
+ }
549
+ },
550
+ interrupt: () => {
551
+ agent.abort();
552
+ agent.clearAllQueues();
553
+ unsubscribe?.();
554
+ },
555
+ partialAssistantText: () => assistantText(agent.state.messages) || undefined,
556
+ };
557
+ }
558
+ requireRunnerState() {
559
+ if (!this.state) {
560
+ throw new Error("Turn runner has not been started.");
561
+ }
562
+ return this.state;
563
+ }
564
+ getState() {
565
+ if (!this.state)
566
+ return undefined;
567
+ return this.snapshotState(this.state);
568
+ }
569
+ snapshotState(state) {
570
+ const parentAgent = this.parentAgent
571
+ ? {
572
+ ...state.agent,
573
+ status: state.agent.status,
574
+ messages: this.parentAgent.state.messages,
575
+ }
576
+ : state.agent;
577
+ return {
578
+ ...state,
579
+ agent: parentAgent,
580
+ stateMachine: this.stateMachineController.getSession(),
581
+ todos: copyOptionalArray(state.todos ?? this.state?.todos),
582
+ followUpQueue: copyOptionalArray(state.followUpQueue ?? this.state?.followUpQueue),
583
+ queuedCommands: copyOptionalArray(state.queuedCommands ?? this.state?.queuedCommands),
584
+ };
585
+ }
586
+ setState(state) {
587
+ this.state = this.snapshotState(state);
588
+ }
589
+ snapshotActiveAgentState() {
590
+ if (!this.state)
591
+ return;
592
+ this.state = this.snapshotState(this.state);
593
+ }
594
+ getFollowUpQueue() {
595
+ return [...(this.state?.followUpQueue ?? [])];
596
+ }
597
+ setFollowUpQueue(prompts) {
598
+ if (!this.state)
599
+ return;
600
+ this.setState({ ...this.state, followUpQueue: [...prompts] });
601
+ }
602
+ getQueuedCommands() {
603
+ return [...(this.state?.queuedCommands ?? [])];
604
+ }
605
+ setQueuedCommands(commands) {
606
+ if (!this.state)
607
+ return;
608
+ this.setState({ ...this.state, queuedCommands: [...commands] });
609
+ }
610
+ shiftQueuedCommand() {
611
+ const commands = this.getQueuedCommands();
612
+ const [command, ...remaining] = commands;
613
+ this.setQueuedCommands(remaining);
614
+ return command;
615
+ }
616
+ getTodos() {
617
+ return [...(this.state?.todos ?? [])];
618
+ }
619
+ setTodos(todos) {
620
+ if (!this.state)
621
+ return;
622
+ this.setState({ ...this.state, todos: [...todos] });
623
+ }
624
+ requireStarted() {
625
+ if (!this.started) {
626
+ throw new Error("Turn runner has not been started.");
627
+ }
628
+ }
629
+ requireParentAgent() {
630
+ if (!this.parentAgent) {
631
+ throw new Error("Turn runner parent agent has not been initialized.");
632
+ }
633
+ return this.parentAgent;
634
+ }
635
+ initializeParentAgent() {
636
+ const state = this.requireRunnerState();
637
+ const appendSystemPrompt = typeof state.mode === "object"
638
+ ? createStateMachineSystemPromptLayer({ mode: state.mode, session: state })
639
+ : undefined;
640
+ this.parentControlResult = { type: "none" };
641
+ this.parentAgent = this.createAgent({
642
+ state,
643
+ appendSystemPrompt,
644
+ ...this.createTools(state.mode),
645
+ }, (result) => {
646
+ this.parentControlResult = result;
647
+ });
648
+ this.replayFollowUpQueueIntoAgent(this.parentAgent);
649
+ this.snapshotActiveAgentState();
650
+ }
651
+ async runTurnRunnerAgentWithStateMachineTools(input) {
652
+ const workerResult = await this.runAgentWorkerWithUsage({
653
+ state: input.state,
654
+ prompt: input.prompt,
655
+ });
656
+ const result = await this.controllerResultFromWorkerResult(workerResult, input.state);
657
+ if (!result)
658
+ return workerResult.terminal;
659
+ return this.driveStateMachineResult(result, workerResult.terminal.state);
660
+ }
661
+ async controllerResultFromWorkerResult(workerResult, state) {
662
+ if (workerResult.control.type === "none")
663
+ return undefined;
664
+ if (workerResult.control.type === "ask_user_question") {
665
+ return { type: "ask", questions: workerResult.control.questions };
666
+ }
667
+ if (workerResult.control.type === "create_state_machine_definition") {
668
+ if (this.stateMachineController.getSession() &&
669
+ !this.stateMachineController.getSession()?.terminal) {
670
+ return {
671
+ type: "terminal",
672
+ status: "failed",
673
+ error: "Cannot create a new state-machine definition while the current state machine is still active.",
674
+ };
675
+ }
676
+ const firstState = workerResult.control.firstState ?? workerResult.control.definition.states[0]?.name ?? "";
677
+ this.stateMachineController.startSession({
678
+ prompt: workerResult.terminal.type === "complete" ? (workerResult.terminal.result ?? "") : "",
679
+ definition: workerResult.control.definition,
680
+ currentState: firstState,
681
+ });
682
+ this.emit({ type: "state_machine", currentState: firstState });
683
+ return this.stateMachineController.runDecision({ kind: "run_state", state: firstState });
684
+ }
685
+ if (workerResult.control.type !== "select_state_machine_state") {
686
+ return {
687
+ type: "terminal",
688
+ status: "failed",
689
+ error: "Unsupported state-machine control result.",
690
+ };
691
+ }
692
+ if (!this.stateMachineController.getSession() &&
693
+ typeof state.mode === "object" &&
694
+ workerResult.control.decision.kind !== "fail") {
695
+ this.stateMachineController.startSession({
696
+ prompt: workerResult.terminal.type === "complete" ? (workerResult.terminal.result ?? "") : "",
697
+ definition: state.mode,
698
+ currentState: workerResult.control.decision.state,
699
+ });
700
+ }
701
+ if (workerResult.control.decision.kind !== "fail") {
702
+ this.emit({ type: "state_machine", currentState: workerResult.control.decision.state });
703
+ }
704
+ return this.stateMachineController.runDecision(workerResult.control.decision);
705
+ }
706
+ createTools(mode) {
707
+ const cwd = this.config.cwd ?? process.cwd();
708
+ const todoStorage = {
709
+ getTodos: () => this.getTodos(),
710
+ setTodos: (todos) => {
711
+ this.setTodos(todos);
712
+ this.emit({ type: "todos", todos });
713
+ },
714
+ };
715
+ const skills = this.skillContext.getSkills();
716
+ if (mode === "agent") {
717
+ return { tools: createDefaultTurnRunnerTools(cwd, todoStorage, skills) };
718
+ }
719
+ return {
720
+ tools: createTurnRunnerTools({
721
+ cwd,
722
+ mode,
723
+ getDefinition: () => this.stateMachineController.getSession()?.definition,
724
+ getStateMachine: () => this.stateMachineController.getSession(),
725
+ getActiveStateOutput: () => this.stateMachineController.getActiveOutput(),
726
+ todoStorage,
727
+ skills,
728
+ }),
729
+ };
730
+ }
731
+ replayFollowUpQueueIntoAgent(agent) {
732
+ for (const prompt of this.getFollowUpQueue()) {
733
+ agent.followUp({
734
+ role: "user",
735
+ content: prompt,
736
+ timestamp: Date.now(),
737
+ });
738
+ }
739
+ }
740
+ async runAgentMode(state, prompt) {
741
+ const workerResult = await this.runAgentWorkerWithUsage({
742
+ state,
743
+ prompt,
744
+ });
745
+ if (workerResult.control.type === "ask_user_question") {
746
+ return this.askUserQuestion(workerResult.terminal, workerResult.control);
747
+ }
748
+ return workerResult.terminal;
749
+ }
750
+ async runAgentWorker(input) {
751
+ if (this.parentAgentRunning) {
752
+ throw new Error("Cannot start a parent agent while another parent agent is active.");
753
+ }
754
+ const agent = this.requireParentAgent();
755
+ this.parentControlResult = { type: "none" };
756
+ const previousMessageCount = agent.state.messages.length;
757
+ this.setParentAgentRunning(true);
758
+ const unsubscribe = agent.subscribe((event) => this.emitAgentEvent(event));
759
+ let interruptedDuringPrompt;
760
+ try {
761
+ await agent.prompt(input.prompt);
762
+ }
763
+ catch (error) {
764
+ interruptedDuringPrompt = this.consumeInterruptedTerminal();
765
+ if (!interruptedDuringPrompt) {
766
+ throw error;
767
+ }
768
+ }
769
+ finally {
770
+ unsubscribe();
771
+ this.setParentAgentRunning(false);
772
+ }
773
+ const interrupted = interruptedDuringPrompt ?? this.consumeInterruptedTerminal();
774
+ if (interrupted) {
775
+ return { control: this.parentControlResult, terminal: interrupted };
776
+ }
777
+ const messages = agent.state.messages;
778
+ const usage = usageFromMessages(messages.slice(previousMessageCount));
779
+ const status = agent.state.errorMessage ? "failed" : "completed";
780
+ const state = {
781
+ ...input.state,
782
+ status,
783
+ agent: {
784
+ ...input.state.agent,
785
+ status,
786
+ messages,
787
+ },
788
+ };
789
+ return {
790
+ control: this.parentControlResult,
791
+ terminal: {
792
+ type: "complete",
793
+ status,
794
+ state,
795
+ result: assistantText(messages),
796
+ error: agent.state.errorMessage,
797
+ usage,
798
+ },
799
+ };
800
+ }
801
+ setParentAgentRunning(running) {
802
+ this.parentAgentRunning = running;
803
+ this.snapshotActiveAgentState();
804
+ }
805
+ async runAgentWorkerWithUsage(input) {
806
+ const result = await this.runAgentWorker(input);
807
+ this.recordUsage(result.terminal.usage);
808
+ return result;
809
+ }
810
+ createAgent(input, onControlResult) {
811
+ const options = this.resolveTurnOptions(undefined, input.state.options);
812
+ const model = resolveModelName(options.model ?? DEFAULT_CLI_MODEL);
813
+ const memoryModel = options.memoryModel ?? DEFAULT_CLI_MEMORY_MODEL;
814
+ // Parent agent configuration is derived from start/session options, not
815
+ // per-prompt command options. Keeping model and prompt shape stable protects
816
+ // prompt caching across all pi-agent turns inside a duet-agent session.
817
+ return new Agent({
818
+ initialState: {
819
+ model,
820
+ thinkingLevel: options.thinkingLevel ?? "medium",
821
+ systemPrompt: this.createBaseSystemPromptWithAppendedLayers({
822
+ append: [input.appendSystemPrompt],
823
+ skills: input.skills,
824
+ }),
825
+ messages: input.state.agent.messages,
826
+ tools: input.tools,
827
+ },
828
+ transformContext: this.createMemoryTransform(memoryModel),
829
+ toolExecution: "parallel",
830
+ afterToolCall: async (context) => {
831
+ const details = context.result.details;
832
+ if (isTurnRunnerControlResult(details)) {
833
+ onControlResult?.(details);
834
+ }
835
+ return undefined;
836
+ },
837
+ getApiKey: getEnvApiKey,
838
+ });
839
+ }
840
+ createMemoryTransform(model) {
841
+ return createObservationalMemoryTransform({
842
+ memory: this.memory,
843
+ actorModel: model,
844
+ settings: this.config.memory,
845
+ onUsage: (usage) => this.recordUsage(usage),
846
+ onActivity: (event) => this.emit({ type: "memory", ...event }),
847
+ });
848
+ }
849
+ async getSkills() {
850
+ await this.ensureSkillsLoaded();
851
+ return this.skillContext.getSkills();
852
+ }
853
+ /** System-prompt files (AGENTS.md by default) that resolved on disk for this session. */
854
+ async getResolvedAgentFiles() {
855
+ await this.ensureSkillsLoaded();
856
+ return this.skillContext.getResolvedAgentFiles();
857
+ }
858
+ /** Skill name collisions where one definition shadowed another during discovery. */
859
+ async getSkillCollisions() {
860
+ await this.ensureSkillsLoaded();
861
+ return this.skillContext.getSkillCollisions();
862
+ }
863
+ getSkillInstructions(skillId) {
864
+ return this.skillContext.getSkillInstructions(skillId);
865
+ }
866
+ async ensureSkillsLoaded() {
867
+ await this.skillContext.ensureLoaded();
868
+ }
869
+ async ensureMemoryLoaded() {
870
+ if (this.memoryLoaded)
871
+ return;
872
+ this.memoryLoaded = true;
873
+ this.memoryDispose = await loadStoredMemory(this.config.memoryDbPath, this.config.cwd ?? process.cwd(), this.memory);
874
+ }
875
+ createBaseSystemPromptWithAppendedLayers(input) {
876
+ return this.skillContext.createSystemPromptWithAppendedLayers(input);
877
+ }
878
+ askUserQuestion(terminal, control) {
879
+ return {
880
+ type: "ask",
881
+ questions: control.questions,
882
+ state: { ...terminal.state, status: "waiting_for_human" },
883
+ };
884
+ }
885
+ resolveTurnOptions(options, base) {
886
+ return {
887
+ model: options?.model ?? base?.model ?? this.config.model ?? DEFAULT_CLI_MODEL,
888
+ memoryModel: options?.memoryModel ??
889
+ base?.memoryModel ??
890
+ this.config.memoryModel ??
891
+ DEFAULT_CLI_MEMORY_MODEL,
892
+ thinkingLevel: options?.thinkingLevel ?? base?.thinkingLevel ?? this.config.thinkingLevel,
893
+ };
894
+ }
895
+ recordUsage(usage) {
896
+ this.turnUsage = addUsage(this.turnUsage, usage);
897
+ }
898
+ emitAgentEvent(event) {
899
+ if (event.type === "message_start" && event.message.role === "user") {
900
+ this.removeFollowUpPrompt(agentMessageText(event.message));
901
+ }
902
+ for (const turnEvent of agentEventToTurnEvents(event)) {
903
+ this.emit(turnEvent);
904
+ }
905
+ }
906
+ }
907
+ //# sourceMappingURL=turn-runner.js.map