@mikode13/harness 11.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +138 -6
  2. package/dist/agent/domain/agent.d.ts +24 -3
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/approval.d.ts +20 -0
  5. package/dist/agent/domain/approval.js.map +1 -1
  6. package/dist/agent/domain/inbox.d.ts +42 -0
  7. package/dist/agent/domain/inbox.js +81 -0
  8. package/dist/agent/domain/inbox.js.map +1 -0
  9. package/dist/agent/domain/oneRunAtATime.d.ts +17 -0
  10. package/dist/agent/domain/oneRunAtATime.js +32 -0
  11. package/dist/agent/domain/oneRunAtATime.js.map +1 -0
  12. package/dist/agent/domain/runContext.d.ts +4 -0
  13. package/dist/agent/domain/runContext.js +5 -0
  14. package/dist/agent/domain/runContext.js.map +1 -1
  15. package/dist/agent/domain/session.d.ts +15 -0
  16. package/dist/agent/domain/session.js +80 -0
  17. package/dist/agent/domain/session.js.map +1 -0
  18. package/dist/engines/claude/infrastructure/model/claudeAgent.d.ts +6 -2
  19. package/dist/engines/claude/infrastructure/model/claudeAgent.js +20 -5
  20. package/dist/engines/claude/infrastructure/model/claudeAgent.js.map +1 -1
  21. package/dist/engines/codex/infrastructure/model/codexAgent.d.ts +5 -1
  22. package/dist/engines/codex/infrastructure/model/codexAgent.js +13 -2
  23. package/dist/engines/codex/infrastructure/model/codexAgent.js.map +1 -1
  24. package/dist/engines/domain/model/backgroundAgents.d.ts +5 -2
  25. package/dist/engines/domain/model/backgroundAgents.js +11 -3
  26. package/dist/engines/domain/model/backgroundAgents.js.map +1 -1
  27. package/dist/engines/domain/model/llmAgent.d.ts +5 -0
  28. package/dist/engines/domain/model/llmAgent.js +68 -10
  29. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  30. package/dist/factory/infrastructure/agentLLMFactory.d.ts +18 -5
  31. package/dist/factory/infrastructure/agentLLMFactory.js +31 -8
  32. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  33. package/dist/index.d.ts +5 -1
  34. package/dist/index.js +3 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +9 -1
  37. package/dist/orchestration/domain/model/orchestratorAgent.js +20 -3
  38. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  39. package/dist/recovery/domain/history.d.ts +10 -3
  40. package/dist/recovery/domain/history.js +1 -0
  41. package/dist/recovery/domain/history.js.map +1 -1
  42. package/dist/recovery/domain/recoveryStore.d.ts +14 -2
  43. package/dist/recovery/domain/recoveryStore.js.map +1 -1
  44. package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +21 -2
  45. package/dist/recovery/infrastructure/fileRecoveryStore.js +58 -6
  46. package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
  47. package/dist/recovery/infrastructure/folderClaims.d.ts +27 -0
  48. package/dist/recovery/infrastructure/folderClaims.js +124 -0
  49. package/dist/recovery/infrastructure/folderClaims.js.map +1 -0
  50. package/dist/shared/domain/session.d.ts +12 -0
  51. package/dist/shared/domain/session.js +2 -0
  52. package/dist/shared/domain/session.js.map +1 -0
  53. package/dist/tools/domain/preparedCall.d.ts +3 -1
  54. package/dist/tools/domain/preparedCall.js.map +1 -1
  55. package/dist/tools/infrastructure/commandTool.js +2 -0
  56. package/dist/tools/infrastructure/commandTool.js.map +1 -1
  57. package/dist/tools/infrastructure/exploreTool.js +1 -1
  58. package/dist/tools/infrastructure/exploreTool.js.map +1 -1
  59. package/dist/tools/infrastructure/fileTools.d.ts +2 -0
  60. package/dist/tools/infrastructure/fileTools.js +9 -1
  61. package/dist/tools/infrastructure/fileTools.js.map +1 -1
  62. package/dist/tools/infrastructure/removeWorktreeTool.d.ts +23 -0
  63. package/dist/tools/infrastructure/removeWorktreeTool.js +172 -0
  64. package/dist/tools/infrastructure/removeWorktreeTool.js.map +1 -0
  65. package/dist/tools/infrastructure/workspaceWrites.d.ts +4 -0
  66. package/dist/tools/infrastructure/workspaceWrites.js +30 -16
  67. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
  68. package/dist/tools/infrastructure/worktrees.d.ts +49 -0
  69. package/dist/tools/infrastructure/worktrees.js +154 -0
  70. package/dist/tools/infrastructure/worktrees.js.map +1 -0
  71. package/dist/tools/infrastructure/writeSession.d.ts +5 -1
  72. package/dist/tools/infrastructure/writeSession.js +7 -3
  73. package/dist/tools/infrastructure/writeSession.js.map +1 -1
  74. package/dist/workflow/domain/workflow.d.ts +3 -0
  75. package/dist/workflow/domain/workflow.js.map +1 -1
  76. package/dist/workflow/domain/workflowRunner.js +8 -2
  77. package/dist/workflow/domain/workflowRunner.js.map +1 -1
  78. package/package.json +1 -1
package/README.md CHANGED
@@ -446,6 +446,54 @@ it to correct. An asker that throws, or does not answer each question with some
446
446
  ends the run with an `UnrecoverableError`. The Agent SDK engines ignore `ask`, and
447
447
  `createLLMOrchestrator` passes it to every role.
448
448
 
449
+ ### One run at a time, and what the user sends meanwhile
450
+
451
+ An agent continues one conversation, so it takes one run at a time: a `run()` that starts
452
+ while another run on the same agent works rejects with `RunInProgressError`, an
453
+ `UnrecoverableError`, and changes nothing. Queue what the user sends meanwhile, and run it
454
+ once the run in progress ends. For one chat, keep one agent; for another chat, create another.
455
+
456
+ A message the user wants read at once goes to the run in progress through an `Inbox`:
457
+
458
+ ```ts
459
+ import { Inbox } from '@mikode13/harness';
460
+
461
+ const inbox = new Inbox();
462
+ const running = agent.run('Add the export button', { signal, inbox });
463
+ // Later, while it works:
464
+ inbox.send('Make it a dropdown too');
465
+ await running;
466
+ // What the run did not keep, such as a message sent after its final answer, is the next turn's.
467
+ const unread = inbox.close();
468
+ ```
469
+
470
+ `createLLMAgent` reads what was sent once the step in progress ends, without cutting a model
471
+ call or a tool, as a message from the user. It also reads it while it waits for agents working
472
+ in the background, so the model can reconsider at once. The Agent SDK engines, the
473
+ orchestrators and the workflows never read it, so `close()` returns everything sent to them. Neither does a
474
+ run's last step, which has no step left to answer it. A run that fails before it is kept gives
475
+ back what it read: a retried attempt reads it again, or `close()` returns it.
476
+
477
+ ### Sessions
478
+
479
+ Every run belongs to a session. Name it in `RunOptions.session`, such as your API's session
480
+ id with a name to show; without one, the run belongs to the agent's own session, which the
481
+ harness makes for each agent with an id a person can read, such as `smart-hawk-4821`.
482
+ `AgentResponse.session` returns the session the run belonged to, so a consumer that builds
483
+ its agent again passes it back to stay in the same session:
484
+
485
+ ```ts
486
+ const first = await agent.run('Add the export button', { signal });
487
+ // Later, maybe with an agent built again:
488
+ await other.run('Make it a dropdown too', { signal, session: first.session });
489
+ ```
490
+
491
+ An agent another agent started belongs to the outer run's session, and a workflow makes a
492
+ new one for each run that names none. A session without a `name`, or with a blank one, is shown by its `id`. Each
493
+ run that writes records its session in the workspace's history, as `HistoryRun.session`, so
494
+ a host can say whose change an undo takes back. It is the harness's session, not a
495
+ provider's, which stays inside its engine.
496
+
449
497
  ### Agents that change files
450
498
 
451
499
  `createFileTools` gives an agent the file tools of its provider, over the folders you
@@ -475,6 +523,11 @@ const agent = createLLMAgent('openai', {
475
523
  });
476
524
  ```
477
525
 
526
+ `workspace` gives no tools: it tells the agent where it works and has it follow the
527
+ history. An agent given a workspace and no tools at all is refused with an
528
+ `InvalidAgentConfigError`; one with only tools of your own is built, and one with neither
529
+ works from its prompt alone.
530
+
478
531
  Every tool goes through one access policy:
479
532
 
480
533
  - it refuses a path outside the roots;
@@ -699,12 +752,13 @@ await history.redo();
699
752
  await history.goTo(historyStart); // as it was before the agent's first run
700
753
  ```
701
754
 
702
- `list()` gives every run kept, oldest first, with its parent, status and the files it
703
- changed, and the run the workspace is at (`head`), absent at the start. Runs form a tree:
704
- going back and writing again starts a new branch, and `goTo` reaches the old one. `redo`
705
- returns towards where the workspace was most recently. A run's `runId` is in its
706
- `AgentResponse` when it changed a file, and a run that failed or was cancelled after
707
- writing is still listed, with that status. A run that changed nothing is not kept.
755
+ `list()` gives every run kept, oldest first, with its parent, status, the files it changed
756
+ and the session it came from, and the run the workspace is at (`head`), absent at the
757
+ start. Runs form a tree: going back and writing again starts a new branch, and `goTo`
758
+ reaches the old one. `redo` returns towards where the workspace was most recently. A
759
+ run's `runId` is in its `AgentResponse` when it changed a file, and a run that failed or
760
+ was cancelled after writing is still listed, with that status. A run that changed nothing
761
+ is not kept.
708
762
 
709
763
  A move writes a file only if it still holds what the history expects. A file you
710
764
  changed since is left as it is and named in the move's `conflicts`, and the move is
@@ -736,6 +790,84 @@ The history is not version control. It holds only what the harness's own tools w
736
790
  not your edits, not other programs', and not the Agent SDK engines' changes. The lock
737
791
  keeps a second harness run out, but not an editor.
738
792
 
793
+ ### Working apart in a worktree
794
+
795
+ Runs that only read never hold the workspace, so several chats can read and plan in one
796
+ folder at once. Only one run at a time writes there. For runs that write side by side,
797
+ give each its own git worktree: a folder of its own, with a history of its own.
798
+
799
+ ```ts
800
+ import { createFileTools, createWorktree, type WorkspaceOptions } from '@mikode13/harness';
801
+
802
+ const { path } = await createWorktree({
803
+ repository: process.cwd(),
804
+ branch: 'feat/export-button',
805
+ from: 'main',
806
+ });
807
+ const workspace: WorkspaceOptions = { roots: [{ path, access: 'write' }] };
808
+ const agent = createLLMAgent('openai', {
809
+ systemPrompt: 'You build the export button, with a test for it.',
810
+ tools: await createFileTools('openai', workspace),
811
+ workspace,
812
+ });
813
+ ```
814
+
815
+ The worktree checks out `branch`, a new branch started at `from`. Without `path` it goes
816
+ beside the repository's main folder, in `../<repository>-worktrees/<branch>`, with any
817
+ `/` in the branch made a `-`. It fails with an `UnrecoverableError` when the branch
818
+ exists or is not a valid name, `from` names no commit, or the folder exists and is not
819
+ empty. A failure leaves no branch or folder of its own behind.
820
+
821
+ Worktrees need git 2.36 or later, the first whose `worktree list` can name a folder whose
822
+ path holds a newline. With an older git, `createWorktree` fails with an
823
+ `UnrecoverableError`, and `removeWorktree: true` fails with an `InvalidAgentConfigError`
824
+ when the tools are created; both name the version found.
825
+
826
+ The harness never removes a worktree. An agent given `removeWorktree: true`, in
827
+ `createFileTools` or `createLLMOrchestrator` for its executor, can remove one when the
828
+ user asks. It removes the folder and every file in it, never the branch, which keeps
829
+ its commits. Each call asks the approver, and the request's `worktree` says what is
830
+ lost: `uncommitted` counts the files with changes no commit has, `ignored` the paths
831
+ `.gitignore` excludes, such as `.env` or `node_modules`, a folder once, and
832
+ `commitsOnlyHere` the commits only a detached HEAD holds. An ignored folder is never
833
+ looked into, so what is written in it while the user is asked goes too: when `ignored`
834
+ is not zero, say so in the question. The tool's answer tells the model the same. The
835
+ tool refuses the repository's main folder, a worktree the agent works in, and a locked
836
+ one. Once approved, it claims the worktree, so no harness run starts writing in it,
837
+ and removes nothing while a run may write there, from the worktree, a folder inside it
838
+ or a second root, or if the worktree changed while the user was asked: the model then
839
+ calls it again, and the user is asked anew.
840
+
841
+ ### Rules for runs that share a folder
842
+
843
+ The harness coordinates its own runs on one folder by these rules. Each refusal says which
844
+ rule stopped it, worded so the model can tell the user why it cannot go on.
845
+
846
+ 1. **An agent takes one run at a time.** A second `run()` on a busy agent rejects with
847
+ `RunInProgressError`. Queue the message, or send it to the run in progress through
848
+ its `Inbox`.
849
+ 2. **Reading never waits.** Any number of runs read and plan in one folder at once.
850
+ 3. **One run at a time writes to a workspace.** A run holds its workspace from its first
851
+ write until it ends, and so does an undo, a redo or a `goTo`. Meanwhile another run's
852
+ write is refused and the model is told why, and a move rejects with
853
+ `WorkspaceBusyError`. For runs that write side by side, give each its own worktree.
854
+ 4. **No worktree is removed while a harness run may write in it,** from the worktree, from
855
+ a folder inside it, or through a second root, and no run starts writing in a worktree
856
+ being removed. An undo or redo protects every file it writes, wherever it is, even
857
+ when the history was opened by its first root alone.
858
+ 5. **In one run, every set of tools that writes to one history may write only where the
859
+ first set may.** A run claims the folders of the tools that wrote first; tools built
860
+ apart, with another `write` root, are refused, and the model is told why. Give every
861
+ set the same `write` roots, or build the widest first.
862
+ 6. **What is removed is what was approved.** If a worktree changed while the user was
863
+ asked, nothing is removed, and the user is asked again. The inside of a folder
864
+ `.gitignore` excludes whole is not watched (rule 7).
865
+ 7. **The harness sees only its own runs** that keep their history in the same state
866
+ directory. An editor, a terminal, another program, an Agent SDK engine or a history
867
+ kept elsewhere is not held back by any of these rules, and the inside of an ignored
868
+ folder is never looked into. Writing by hand in a folder an agent is writing in can
869
+ overwrite either side; keeping them apart is up to whoever does it.
870
+
739
871
  ## Tests
740
872
 
741
873
  `pnpm test` runs the unit suite against deterministic fakes; it never contacts a
@@ -1,6 +1,8 @@
1
1
  import type { Tokens } from '#src/shared/domain/tokens';
2
2
  import type { Approver } from './approval.ts';
3
+ import type { Inbox } from './inbox.ts';
3
4
  import type { Asker } from './question.ts';
5
+ import type { Session } from '#src/shared/domain/session';
4
6
  /**
5
7
  * How a run ended when it did not fail. Every token a run spends travels with its end: here
6
8
  * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.
@@ -21,13 +23,19 @@ export interface AgentResponse {
21
23
  * the history does not record.
22
24
  */
23
25
  runId?: string;
26
+ /**
27
+ * The session the run belonged to: the one `RunOptions.session` gave, or the agent's own,
28
+ * which the harness creates for each agent. Pass it to later runs, on this agent or one built
29
+ * again, to keep them in the same session. Every agent the harness builds sets it.
30
+ */
31
+ session?: Session;
24
32
  }
25
33
  export type Callback = (item: ProgressEvent) => void;
26
34
  /**
27
35
  * What a run needs besides its prompt. InstructedAgent passes it on whole. OrchestratorAgent
28
- * copies its own enumerable fields and replaces only `onProgress` per role; RetryingAgent does
29
- * the same for a run with `ask`, replacing only `ask`. A field must therefore be an own
30
- * enumerable one to reach every agent.
36
+ * copies its own enumerable fields, replaces `onProgress` per role and drops `inbox`;
37
+ * RetryingAgent does the same for a run with `ask`, replacing only `ask`. A field must therefore
38
+ * be an own enumerable one to reach every agent.
31
39
  */
32
40
  export interface RunOptions {
33
41
  /**
@@ -51,6 +59,19 @@ export interface RunOptions {
51
59
  * its answer.
52
60
  */
53
61
  ask?: Asker | undefined;
62
+ /**
63
+ * Messages sent to the run while it works. A model-backed agent reads them once the step in
64
+ * progress ends, as a message from the user, and wakes for them while it waits for agents in
65
+ * the background. The Agent SDK engines, the orchestrators and the workflows ignore it, so
66
+ * `Inbox.close` returns everything sent to them.
67
+ */
68
+ inbox?: Inbox | undefined;
69
+ /**
70
+ * The session this run belongs to, such as the API's, recorded with each run that writes.
71
+ * Without it, the run belongs to the agent's own session, the same for each of its runs, which
72
+ * `AgentResponse.session` returns. An agent another agent started belongs to the outer run's.
73
+ */
74
+ session?: Session | undefined;
54
75
  }
55
76
  /** The `onProgress` of a run that gave none. */
56
77
  export declare const ignoreProgress: Callback;
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AA0DA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\nimport type { Asker } from './question.ts';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n\t/**\n\t * The run the workspace's history recorded, when this run wrote through the harness's write\n\t * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes\n\t * the history does not record.\n\t */\n\trunId?: string;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/**\n * What a run needs besides its prompt. InstructedAgent passes it on whole. OrchestratorAgent\n * copies its own enumerable fields and replaces only `onProgress` per role; RetryingAgent does\n * the same for a run with `ask`, replacing only `ask`. A field must therefore be an own\n * enumerable one to reach every agent.\n */\nexport interface RunOptions {\n\t/**\n\t * Cancels the run, which then rejects with the signal's reason: an `AbortError`, or a\n\t * `TimeoutError` from `AbortSignal.timeout`. `isAbortError` recognizes both.\n\t */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide. Agents a delegation runs at once may call it before\n\t * an earlier call has its answer.\n\t */\n\tapprove?: Approver | undefined;\n\t/**\n\t * Answers what a model-backed agent asks the user, through its `askUser` tool, when a\n\t * decision is the user's to make. Without it the agent has no such tool. The Agent SDK\n\t * engines ignore it. Agents a delegation runs at once may call it before an earlier call has\n\t * its answer.\n\t */\n\task?: Asker | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number; agent?: ProgressAgent }\n\t| { type: 'reasoning'; message: string; agent?: ProgressAgent }\n\t| { type: 'search'; query: string; agent?: ProgressAgent }\n\t| {\n\t\t\ttype: 'fileChange';\n\t\t\tchanges: { path: string; kind: 'add' | 'update' | 'delete' }[];\n\t\t\tagent?: ProgressAgent;\n\t }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string; agent?: ProgressAgent }\n\t| { type: 'agentMessage'; message: string; agent?: ProgressAgent }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tagent?: ProgressAgent;\n\t\t\t/** A short, tool-owned description of the call. Narration only. */\n\t\t\tsummary?: string;\n\t\t\t/** The exit code of a configured command that ended with an error. Narration only. */\n\t\t\texitCode?: number;\n\t\t\t/**\n\t\t\t * On `error`, for an agent on the model APIs: what the model was told went wrong, such\n\t\t\t * as a patch that did not apply or a failed command's output. Narration only.\n\t\t\t */\n\t\t\terror?: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it. `sent` when\n\t\t\t * it sent subagents to work in the background, such as an `explore` call: they go on\n\t\t\t * after it, as `subagent` events, until a `subagentEnded` for each.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied' | 'sent';\n\t\t\t/**\n\t\t\t * On `completed`, for a call that changed a file through the harness's write tools: the\n\t\t\t * change as a unified diff, for the person watching. Narration only; the run's whole\n\t\t\t * record is the history's `changes`.\n\t\t\t */\n\t\t\tdiff?: string;\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[]; agent?: ProgressAgent }\n\t| { type: 'turnStarted'; agent?: ProgressAgent }\n\t| { type: 'turnEnded'; agent?: ProgressAgent }\n\t/**\n\t * One billed call to the model by an agent on the model APIs, including one the model\n\t * stopped early or the client turned into an error: what the call sent and what it cost.\n\t * Narration only: the run's total is `AgentResponse.tokens`, or the failure's `tokens`.\n\t */\n\t| {\n\t\t\ttype: 'step';\n\t\t\t/** 1 for the first call of the run. */\n\t\t\tstep: number;\n\t\t\t/** Missing when the provider did not report this call's usage. */\n\t\t\ttokens?: Tokens;\n\t\t\tcontext: StepContext;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A workflow's stage asks its agent for an answer; `attempt` is 2 or more when the previous\n\t * answer was invalid. Stages run one at a time, so the events that follow, until the next\n\t * workflow event, are this stage's agent's.\n\t */\n\t| { type: 'workflowStage'; stage: string; attempt: number; agent?: ProgressAgent }\n\t/** The transition a stage's valid answer selected, and the stage it leads to. */\n\t| {\n\t\t\ttype: 'workflowTransition';\n\t\t\tstage: string;\n\t\t\ttransition: 'go' | 'stop' | 'complete';\n\t\t\t/** Absent after `stop` or `complete`. */\n\t\t\tnextStage?: string;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * An event of a subagent that a tool call started: one of the explorers an `explore` call\n\t * sends, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that\n\t * call's `tool` events, and `subagent` numbers its subagents from 1, so the events of agents\n\t * running in parallel stay apart. Subagents work in the background: the call's own `tool`\n\t * event ends with `status: 'sent'`, and each subagent's events keep arriving after it, until\n\t * its `subagentEnded`, whether it answered, failed or was stopped. A subagent's own subagents\n\t * arrive as `subagent` events inside this one's `event`.\n\t */\n\t| {\n\t\t\ttype: 'subagent';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\t/** Which agent it is: `explorer`, or the name of the tool that delegates to it. */\n\t\t\tname: string;\n\t\t\t/** What the subagent was asked. */\n\t\t\ttask: string;\n\t\t\tevent: ProgressEvent;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A subagent that a call with `status: 'sent'` sent has ended: it `answered`, it `failed` and\n\t * its answer says why, or it was `stopped` because the run ended. `callId`, `subagent` and\n\t * `name` are those of its `subagent` events. Narration only: its answer reaches the model that\n\t * sent it, not the consumer.\n\t */\n\t| {\n\t\t\ttype: 'subagentEnded';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\tname: string;\n\t\t\toutcome: 'answered' | 'failed' | 'stopped';\n\t\t\tagent?: ProgressAgent;\n\t };\n\n/**\n * What one call to the model sent, in characters by where it came from: an estimate of size\n * for finding what fills a context, not a token count.\n */\nexport interface StepContext {\n\t/** The system prompt. */\n\tinstructions: number;\n\t/** The tool definitions. */\n\ttools: number;\n\t/**\n\t * The text of the user's prompts and the model's answers, and the provider's summary of the\n\t * turns before it once it has compacted them: only what the call sent.\n\t */\n\tmessages: number;\n\t/** Reasoning the model produced earlier and is sent back: its provider's own signed or encrypted blocks. */\n\treasoning: number;\n\t/** The input of every tool call so far. */\n\ttoolCalls: number;\n\t/** Every tool result so far, by tool, largest first. */\n\ttoolResults: { tool: string; characters: number; calls: number }[];\n\t/** The largest single results, each with its call's summary when the tool gives one. */\n\tlargestResults: { tool: string; characters: number; summary?: string }[];\n}\n\n/** A role in the fixed planner → executor → reviewer workflow. */\nexport type ProgressAgent = 'planner' | 'executor' | 'reviewer';\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError`, or the `TimeoutError` of\n * `AbortSignal.timeout`, must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AA+EA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\nimport type { Inbox } from './inbox.ts';\nimport type { Asker } from './question.ts';\nimport type { Session } from '#src/shared/domain/session';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n\t/**\n\t * The run the workspace's history recorded, when this run wrote through the harness's write\n\t * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes\n\t * the history does not record.\n\t */\n\trunId?: string;\n\t/**\n\t * The session the run belonged to: the one `RunOptions.session` gave, or the agent's own,\n\t * which the harness creates for each agent. Pass it to later runs, on this agent or one built\n\t * again, to keep them in the same session. Every agent the harness builds sets it.\n\t */\n\tsession?: Session;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/**\n * What a run needs besides its prompt. InstructedAgent passes it on whole. OrchestratorAgent\n * copies its own enumerable fields, replaces `onProgress` per role and drops `inbox`;\n * RetryingAgent does the same for a run with `ask`, replacing only `ask`. A field must therefore\n * be an own enumerable one to reach every agent.\n */\nexport interface RunOptions {\n\t/**\n\t * Cancels the run, which then rejects with the signal's reason: an `AbortError`, or a\n\t * `TimeoutError` from `AbortSignal.timeout`. `isAbortError` recognizes both.\n\t */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide. Agents a delegation runs at once may call it before\n\t * an earlier call has its answer.\n\t */\n\tapprove?: Approver | undefined;\n\t/**\n\t * Answers what a model-backed agent asks the user, through its `askUser` tool, when a\n\t * decision is the user's to make. Without it the agent has no such tool. The Agent SDK\n\t * engines ignore it. Agents a delegation runs at once may call it before an earlier call has\n\t * its answer.\n\t */\n\task?: Asker | undefined;\n\t/**\n\t * Messages sent to the run while it works. A model-backed agent reads them once the step in\n\t * progress ends, as a message from the user, and wakes for them while it waits for agents in\n\t * the background. The Agent SDK engines, the orchestrators and the workflows ignore it, so\n\t * `Inbox.close` returns everything sent to them.\n\t */\n\tinbox?: Inbox | undefined;\n\t/**\n\t * The session this run belongs to, such as the API's, recorded with each run that writes.\n\t * Without it, the run belongs to the agent's own session, the same for each of its runs, which\n\t * `AgentResponse.session` returns. An agent another agent started belongs to the outer run's.\n\t */\n\tsession?: Session | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number; agent?: ProgressAgent }\n\t| { type: 'reasoning'; message: string; agent?: ProgressAgent }\n\t| { type: 'search'; query: string; agent?: ProgressAgent }\n\t| {\n\t\t\ttype: 'fileChange';\n\t\t\tchanges: { path: string; kind: 'add' | 'update' | 'delete' }[];\n\t\t\tagent?: ProgressAgent;\n\t }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string; agent?: ProgressAgent }\n\t| { type: 'agentMessage'; message: string; agent?: ProgressAgent }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tagent?: ProgressAgent;\n\t\t\t/** A short, tool-owned description of the call. Narration only. */\n\t\t\tsummary?: string;\n\t\t\t/** The exit code of a configured command that ended with an error. Narration only. */\n\t\t\texitCode?: number;\n\t\t\t/**\n\t\t\t * On `error`, for an agent on the model APIs: what the model was told went wrong, such\n\t\t\t * as a patch that did not apply or a failed command's output. Narration only.\n\t\t\t */\n\t\t\terror?: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it. `sent` when\n\t\t\t * it sent subagents to work in the background, such as an `explore` call: they go on\n\t\t\t * after it, as `subagent` events, until a `subagentEnded` for each.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied' | 'sent';\n\t\t\t/**\n\t\t\t * On `completed`, for a call that changed a file through the harness's write tools: the\n\t\t\t * change as a unified diff, for the person watching. Narration only; the run's whole\n\t\t\t * record is the history's `changes`.\n\t\t\t */\n\t\t\tdiff?: string;\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[]; agent?: ProgressAgent }\n\t| { type: 'turnStarted'; agent?: ProgressAgent }\n\t| { type: 'turnEnded'; agent?: ProgressAgent }\n\t/**\n\t * One billed call to the model by an agent on the model APIs, including one the model\n\t * stopped early or the client turned into an error: what the call sent and what it cost.\n\t * Narration only: the run's total is `AgentResponse.tokens`, or the failure's `tokens`.\n\t */\n\t| {\n\t\t\ttype: 'step';\n\t\t\t/** 1 for the first call of the run. */\n\t\t\tstep: number;\n\t\t\t/** Missing when the provider did not report this call's usage. */\n\t\t\ttokens?: Tokens;\n\t\t\tcontext: StepContext;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A workflow's stage asks its agent for an answer; `attempt` is 2 or more when the previous\n\t * answer was invalid. Stages run one at a time, so the events that follow, until the next\n\t * workflow event, are this stage's agent's.\n\t */\n\t| { type: 'workflowStage'; stage: string; attempt: number; agent?: ProgressAgent }\n\t/** The transition a stage's valid answer selected, and the stage it leads to. */\n\t| {\n\t\t\ttype: 'workflowTransition';\n\t\t\tstage: string;\n\t\t\ttransition: 'go' | 'stop' | 'complete';\n\t\t\t/** Absent after `stop` or `complete`. */\n\t\t\tnextStage?: string;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * An event of a subagent that a tool call started: one of the explorers an `explore` call\n\t * sends, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that\n\t * call's `tool` events, and `subagent` numbers its subagents from 1, so the events of agents\n\t * running in parallel stay apart. Subagents work in the background: the call's own `tool`\n\t * event ends with `status: 'sent'`, and each subagent's events keep arriving after it, until\n\t * its `subagentEnded`, whether it answered, failed or was stopped. A subagent's own subagents\n\t * arrive as `subagent` events inside this one's `event`.\n\t */\n\t| {\n\t\t\ttype: 'subagent';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\t/** Which agent it is: `explorer`, or the name of the tool that delegates to it. */\n\t\t\tname: string;\n\t\t\t/** What the subagent was asked. */\n\t\t\ttask: string;\n\t\t\tevent: ProgressEvent;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A subagent that a call with `status: 'sent'` sent has ended: it `answered`, it `failed` and\n\t * its answer says why, or it was `stopped` because the run ended. `callId`, `subagent` and\n\t * `name` are those of its `subagent` events. Narration only: its answer reaches the model that\n\t * sent it, not the consumer.\n\t */\n\t| {\n\t\t\ttype: 'subagentEnded';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\tname: string;\n\t\t\toutcome: 'answered' | 'failed' | 'stopped';\n\t\t\tagent?: ProgressAgent;\n\t };\n\n/**\n * What one call to the model sent, in characters by where it came from: an estimate of size\n * for finding what fills a context, not a token count.\n */\nexport interface StepContext {\n\t/** The system prompt. */\n\tinstructions: number;\n\t/** The tool definitions. */\n\ttools: number;\n\t/**\n\t * The text of the user's prompts and the model's answers, and the provider's summary of the\n\t * turns before it once it has compacted them: only what the call sent.\n\t */\n\tmessages: number;\n\t/** Reasoning the model produced earlier and is sent back: its provider's own signed or encrypted blocks. */\n\treasoning: number;\n\t/** The input of every tool call so far. */\n\ttoolCalls: number;\n\t/** Every tool result so far, by tool, largest first. */\n\ttoolResults: { tool: string; characters: number; calls: number }[];\n\t/** The largest single results, each with its call's summary when the tool gives one. */\n\tlargestResults: { tool: string; characters: number; summary?: string }[];\n}\n\n/** A role in the fixed planner → executor → reviewer workflow. */\nexport type ProgressAgent = 'planner' | 'executor' | 'reviewer';\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError`, or the `TimeoutError` of\n * `AbortSignal.timeout`, must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
@@ -12,6 +12,24 @@ export interface CommandLine {
12
12
  /** Where it runs, as a real path. */
13
13
  cwd: string;
14
14
  }
15
+ /** A worktree a tool call will remove, and what is lost with it. */
16
+ export interface WorktreeRemoval {
17
+ /** Its folder, as a real path. */
18
+ path: string;
19
+ /** The branch it has checked out, which stays with its commits; absent when detached. */
20
+ branch?: string;
21
+ /** How many files hold changes no commit has, untracked ones included: they are lost. */
22
+ uncommitted: number;
23
+ /**
24
+ * How many paths `.gitignore` excludes, such as `.env` or `node_modules`, a folder excluded
25
+ * whole counting once: they are lost too. Such a folder is never looked into, so what is
26
+ * written in it while the user is asked goes with it unannounced; a host's question says so
27
+ * when there are any.
28
+ */
29
+ ignored: number;
30
+ /** With a detached HEAD, how many of its commits no branch, tag or remote holds: lost too. */
31
+ commitsOnlyHere: number;
32
+ }
15
33
  /** A tool call waiting for someone to allow it. */
16
34
  export interface ApprovalRequest {
17
35
  tool: string;
@@ -20,6 +38,8 @@ export interface ApprovalRequest {
20
38
  risk: ToolRisk;
21
39
  /** For a call that runs a configured command: the program, worked out from the input. */
22
40
  command?: CommandLine;
41
+ /** For a call that removes a worktree: which one, and what is lost with it. */
42
+ worktree?: WorktreeRemoval;
23
43
  }
24
44
  /** A denial's `reason` reaches the model, so it can try something else. */
25
45
  export type ApprovalDecision = {
@@ -1 +1 @@
1
- {"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"AA6CA;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAChC,GAGyD,EACzD,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAqD,EAAE;IAEtF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QACxB,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAE/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1C,0FAA0F;QAC1F,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,8FAA8F;QAC9F,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACxC,IAAI,YAAY,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,OAAO,QAAQ,CAAC;IACjB,CAAC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAe;IAC3C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAkD,CAAC;IAChF,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,QAAQ;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,YAAY,CAAC,MAAe;IACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAgC,CAAC;IACtD,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC7D,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,QAAQ,KAAK,IAAI,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * How much one tool call can hurt, judged by the tool from that call's input:\n * - `safe` changes nothing, as with a read.\n * - `mutating` makes a change git can undo, as with creating a file.\n * - `destructive` loses something, as with overwriting or deleting a file.\n */\nexport type ToolRisk = 'safe' | 'mutating' | 'destructive';\n\n/** A program a tool call will run, exactly as it will run: no shell reads it. */\nexport interface CommandLine {\n\texecutable: string;\n\targs: string[];\n\t/** Where it runs, as a real path. */\n\tcwd: string;\n}\n\n/** A tool call waiting for someone to allow it. */\nexport interface ApprovalRequest {\n\ttool: string;\n\t/** What the model sent, unvalidated: the tool validates it only if the call is allowed. */\n\tinput: unknown;\n\trisk: ToolRisk;\n\t/** For a call that runs a configured command: the program, worked out from the input. */\n\tcommand?: CommandLine;\n}\n\n/** A denial's `reason` reaches the model, so it can try something else. */\nexport type ApprovalDecision =\n\t{ approved: true } | { approved: false; reason?: string | undefined };\n\n/**\n * Decides one tool call at a time. A consumer that wants \"don't ask again\" remembers its own\n * answers, so the harness keeps no session state. A throw ends the run as an\n * `UnrecoverableError`.\n */\nexport type Approver = (\n\trequest: ApprovalRequest,\n\tsignal: AbortSignal,\n) => ApprovalDecision | Promise<ApprovalDecision>;\n\n/** What `rememberApprovals` asks for: `remember` allows the same call again without asking. */\nexport type RememberableDecision =\n\t| { approved: true; remember?: boolean | undefined }\n\t| { approved: false; reason?: string | undefined };\n\n/**\n * Wraps `ask` so a call the user allowed with `remember` is not asked about again. `key` decides\n * what counts as the same call; it defaults to the tool's name, so \"always\" allows every\n * destructive call to that tool. The memory lives in the returned approver, so its lifetime is\n * the consumer's choice: one per session, one per user, or one per run. Calls made at once are\n * each asked; a call that waits its turn in front of it finds an \"always\" given meanwhile.\n */\nexport function rememberApprovals(\n\task: (\n\t\trequest: ApprovalRequest,\n\t\tsignal: AbortSignal,\n\t) => RememberableDecision | Promise<RememberableDecision>,\n\t{ key = request => request.tool }: { key?: (request: ApprovalRequest) => string } = {},\n): Approver {\n\tconst allowed = new Set<string>();\n\n\treturn async (request, signal) => {\n\t\tconst id = key(request);\n\t\tif (allowed.has(id)) return { approved: true };\n\n\t\tconst answer = await ask(request, signal);\n\t\t// The agent discards an answer that arrives after a cancellation, so it must not be kept.\n\t\tsignal.throwIfAborted();\n\t\t// Read before keeping anything: a truthy `'false'` would otherwise be allowed and remembered.\n\t\tconst decision = readDecision(answer);\n\t\tif (!decision.approved) return decision;\n\t\tif (readRemember(answer)) allowed.add(id);\n\t\treturn decision;\n\t};\n}\n\n/**\n * Reads an approver's answer without trusting its type, since a JavaScript consumer can return\n * anything: only an `approved` that is a boolean decides, and anything else throws.\n */\nexport function readDecision(answer: unknown): ApprovalDecision {\n\tif (typeof answer !== 'object' || answer === null) {\n\t\tthrow new TypeError('The approver answered without a decision');\n\t}\n\tconst { approved, reason } = answer as { approved?: unknown; reason?: unknown };\n\tif (typeof approved !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `approved`');\n\t}\n\n\treturn approved\n\t\t? { approved }\n\t\t: { approved, reason: typeof reason === 'string' ? reason : undefined };\n}\n\nfunction readRemember(answer: unknown): boolean {\n\tconst { remember } = answer as { remember?: unknown };\n\tif (remember !== undefined && typeof remember !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `remember`');\n\t}\n\treturn remember === true;\n}\n"]}
1
+ {"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"AAkEA;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAChC,GAGyD,EACzD,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAqD,EAAE;IAEtF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QACxB,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAE/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1C,0FAA0F;QAC1F,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,8FAA8F;QAC9F,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACxC,IAAI,YAAY,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,OAAO,QAAQ,CAAC;IACjB,CAAC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAe;IAC3C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAkD,CAAC;IAChF,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,QAAQ;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,YAAY,CAAC,MAAe;IACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAgC,CAAC;IACtD,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC7D,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,QAAQ,KAAK,IAAI,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * How much one tool call can hurt, judged by the tool from that call's input:\n * - `safe` changes nothing, as with a read.\n * - `mutating` makes a change git can undo, as with creating a file.\n * - `destructive` loses something, as with overwriting or deleting a file.\n */\nexport type ToolRisk = 'safe' | 'mutating' | 'destructive';\n\n/** A program a tool call will run, exactly as it will run: no shell reads it. */\nexport interface CommandLine {\n\texecutable: string;\n\targs: string[];\n\t/** Where it runs, as a real path. */\n\tcwd: string;\n}\n\n/** A worktree a tool call will remove, and what is lost with it. */\nexport interface WorktreeRemoval {\n\t/** Its folder, as a real path. */\n\tpath: string;\n\t/** The branch it has checked out, which stays with its commits; absent when detached. */\n\tbranch?: string;\n\t/** How many files hold changes no commit has, untracked ones included: they are lost. */\n\tuncommitted: number;\n\t/**\n\t * How many paths `.gitignore` excludes, such as `.env` or `node_modules`, a folder excluded\n\t * whole counting once: they are lost too. Such a folder is never looked into, so what is\n\t * written in it while the user is asked goes with it unannounced; a host's question says so\n\t * when there are any.\n\t */\n\tignored: number;\n\t/** With a detached HEAD, how many of its commits no branch, tag or remote holds: lost too. */\n\tcommitsOnlyHere: number;\n}\n\n/** A tool call waiting for someone to allow it. */\nexport interface ApprovalRequest {\n\ttool: string;\n\t/** What the model sent, unvalidated: the tool validates it only if the call is allowed. */\n\tinput: unknown;\n\trisk: ToolRisk;\n\t/** For a call that runs a configured command: the program, worked out from the input. */\n\tcommand?: CommandLine;\n\t/** For a call that removes a worktree: which one, and what is lost with it. */\n\tworktree?: WorktreeRemoval;\n}\n\n/** A denial's `reason` reaches the model, so it can try something else. */\nexport type ApprovalDecision =\n\t{ approved: true } | { approved: false; reason?: string | undefined };\n\n/**\n * Decides one tool call at a time. A consumer that wants \"don't ask again\" remembers its own\n * answers, so the harness keeps no session state. A throw ends the run as an\n * `UnrecoverableError`.\n */\nexport type Approver = (\n\trequest: ApprovalRequest,\n\tsignal: AbortSignal,\n) => ApprovalDecision | Promise<ApprovalDecision>;\n\n/** What `rememberApprovals` asks for: `remember` allows the same call again without asking. */\nexport type RememberableDecision =\n\t| { approved: true; remember?: boolean | undefined }\n\t| { approved: false; reason?: string | undefined };\n\n/**\n * Wraps `ask` so a call the user allowed with `remember` is not asked about again. `key` decides\n * what counts as the same call; it defaults to the tool's name, so \"always\" allows every\n * destructive call to that tool. The memory lives in the returned approver, so its lifetime is\n * the consumer's choice: one per session, one per user, or one per run. Calls made at once are\n * each asked; a call that waits its turn in front of it finds an \"always\" given meanwhile.\n */\nexport function rememberApprovals(\n\task: (\n\t\trequest: ApprovalRequest,\n\t\tsignal: AbortSignal,\n\t) => RememberableDecision | Promise<RememberableDecision>,\n\t{ key = request => request.tool }: { key?: (request: ApprovalRequest) => string } = {},\n): Approver {\n\tconst allowed = new Set<string>();\n\n\treturn async (request, signal) => {\n\t\tconst id = key(request);\n\t\tif (allowed.has(id)) return { approved: true };\n\n\t\tconst answer = await ask(request, signal);\n\t\t// The agent discards an answer that arrives after a cancellation, so it must not be kept.\n\t\tsignal.throwIfAborted();\n\t\t// Read before keeping anything: a truthy `'false'` would otherwise be allowed and remembered.\n\t\tconst decision = readDecision(answer);\n\t\tif (!decision.approved) return decision;\n\t\tif (readRemember(answer)) allowed.add(id);\n\t\treturn decision;\n\t};\n}\n\n/**\n * Reads an approver's answer without trusting its type, since a JavaScript consumer can return\n * anything: only an `approved` that is a boolean decides, and anything else throws.\n */\nexport function readDecision(answer: unknown): ApprovalDecision {\n\tif (typeof answer !== 'object' || answer === null) {\n\t\tthrow new TypeError('The approver answered without a decision');\n\t}\n\tconst { approved, reason } = answer as { approved?: unknown; reason?: unknown };\n\tif (typeof approved !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `approved`');\n\t}\n\n\treturn approved\n\t\t? { approved }\n\t\t: { approved, reason: typeof reason === 'string' ? reason : undefined };\n}\n\nfunction readRemember(answer: unknown): boolean {\n\tconst { remember } = answer as { remember?: unknown };\n\tif (remember !== undefined && typeof remember !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `remember`');\n\t}\n\treturn remember === true;\n}\n"]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Messages a consumer sends to a run while it works, such as what the user typed meanwhile and
3
+ * asked to have read at once. A model-backed agent reads them once the step in progress ends,
4
+ * and wakes for them while it waits for the agents it sent to work in the background. The Agent
5
+ * SDK engines, the orchestrators and the workflows never read them.
6
+ *
7
+ * One inbox belongs to one run: create it for the run, pass it in `RunOptions`, and close it
8
+ * once the run ends.
9
+ */
10
+ export declare class Inbox {
11
+ constructor();
12
+ /**
13
+ * Sends a message to the run. False once the inbox is closed, whatever the text: no run will
14
+ * read it.
15
+ *
16
+ * @throws {RangeError} when the inbox is open and `text` holds nothing but whitespace, which a
17
+ * provider may refuse.
18
+ */
19
+ send(text: string): boolean;
20
+ /**
21
+ * Closes the inbox, and returns every message the run did not keep, in the order they were
22
+ * sent: those it never read, such as one sent after its final answer, and those it read in an
23
+ * attempt that failed. They belong to the next run.
24
+ */
25
+ close(): string[];
26
+ }
27
+ /** How a run reads its inbox. Internal. */
28
+ export interface InboxReader {
29
+ /** Takes every message waiting, in the order they were sent. */
30
+ take(): string[];
31
+ /** Gives back messages a run read but did not keep, ahead of any sent since. */
32
+ giveBack(messages: readonly string[]): void;
33
+ /** Calls `listener` for each message sent, until the returned function is called. */
34
+ onMessage(listener: () => void): () => void;
35
+ }
36
+ /**
37
+ * How a run reads `inbox`, which comes untyped from a consumer's options.
38
+ *
39
+ * @throws {UnrecoverableError} for anything that is not an `Inbox` of this package, such as a
40
+ * plain object, or one made by another copy of the package.
41
+ */
42
+ export declare function readerOf(inbox: unknown): InboxReader;
@@ -0,0 +1,81 @@
1
+ import { UnrecoverableError } from '#src/shared/domain/errors';
2
+ /**
3
+ * Messages a consumer sends to a run while it works, such as what the user typed meanwhile and
4
+ * asked to have read at once. A model-backed agent reads them once the step in progress ends,
5
+ * and wakes for them while it waits for the agents it sent to work in the background. The Agent
6
+ * SDK engines, the orchestrators and the workflows never read them.
7
+ *
8
+ * One inbox belongs to one run: create it for the run, pass it in `RunOptions`, and close it
9
+ * once the run ends.
10
+ */
11
+ export class Inbox {
12
+ constructor() {
13
+ states.set(this, { waiting: [], closed: false, listeners: new Set() });
14
+ }
15
+ /**
16
+ * Sends a message to the run. False once the inbox is closed, whatever the text: no run will
17
+ * read it.
18
+ *
19
+ * @throws {RangeError} when the inbox is open and `text` holds nothing but whitespace, which a
20
+ * provider may refuse.
21
+ */
22
+ send(text) {
23
+ const state = stateOf(this);
24
+ if (state.closed)
25
+ return false;
26
+ if (text.trim() === '')
27
+ throw new RangeError('A message to a run needs some text');
28
+ state.waiting.push(text);
29
+ for (const listener of state.listeners)
30
+ listener();
31
+ return true;
32
+ }
33
+ /**
34
+ * Closes the inbox, and returns every message the run did not keep, in the order they were
35
+ * sent: those it never read, such as one sent after its final answer, and those it read in an
36
+ * attempt that failed. They belong to the next run.
37
+ */
38
+ close() {
39
+ const state = stateOf(this);
40
+ state.closed = true;
41
+ state.listeners.clear();
42
+ return state.waiting.splice(0);
43
+ }
44
+ }
45
+ // Kept apart from the class, so a consumer sees only `send` and `close`.
46
+ const states = new WeakMap();
47
+ function stateOf(inbox) {
48
+ const state = states.get(inbox);
49
+ if (!state)
50
+ throw new TypeError('Not an Inbox');
51
+ return state;
52
+ }
53
+ /**
54
+ * How a run reads `inbox`, which comes untyped from a consumer's options.
55
+ *
56
+ * @throws {UnrecoverableError} for anything that is not an `Inbox` of this package, such as a
57
+ * plain object, or one made by another copy of the package.
58
+ */
59
+ export function readerOf(inbox) {
60
+ const state = typeof inbox === 'object' && inbox !== null ? states.get(inbox) : undefined;
61
+ if (!state) {
62
+ throw new UnrecoverableError('The run was given an inbox that is not an Inbox', {
63
+ cause: 'RunOptions.inbox, when given, is an Inbox this package made.',
64
+ });
65
+ }
66
+ return {
67
+ take: () => state.waiting.splice(0),
68
+ giveBack: messages => {
69
+ state.waiting.unshift(...messages);
70
+ },
71
+ onMessage: listener => {
72
+ if (state.closed)
73
+ return () => undefined;
74
+ state.listeners.add(listener);
75
+ return () => {
76
+ state.listeners.delete(listener);
77
+ };
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=inbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../../src/agent/domain/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,OAAO,KAAK;IACjB;QACC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IACxE,CAAC;IAED;;;;;;OAMG;IACH,IAAI,CAAC,IAAY;QAChB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC/B,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,MAAM,IAAI,UAAU,CAAC,oCAAoC,CAAC,CAAC;QACnF,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,SAAS;YAAE,QAAQ,EAAE,CAAC;QACnD,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;OAIG;IACH,KAAK;QACJ,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;CACD;AAQD,yEAAyE;AACzE,MAAM,MAAM,GAAG,IAAI,OAAO,EAAqB,CAAC;AAEhD,SAAS,OAAO,CAAC,KAAY;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,SAAS,CAAC,cAAc,CAAC,CAAC;IAChD,OAAO,KAAK,CAAC;AACd,CAAC;AAYD;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc;IACtC,MAAM,KAAK,GACV,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAc,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACtF,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,kBAAkB,CAAC,iDAAiD,EAAE;YAC/E,KAAK,EAAE,8DAA8D;SACrE,CAAC,CAAC;IACJ,CAAC;IACD,OAAO;QACN,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACnC,QAAQ,EAAE,QAAQ,CAAC,EAAE;YACpB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;QACpC,CAAC;QACD,SAAS,EAAE,QAAQ,CAAC,EAAE;YACrB,IAAI,KAAK,CAAC,MAAM;gBAAE,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;YACzC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC9B,OAAO,GAAG,EAAE;gBACX,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAClC,CAAC,CAAC;QACH,CAAC;KACD,CAAC;AACH,CAAC","sourcesContent":["import { UnrecoverableError } from '#src/shared/domain/errors';\n\n/**\n * Messages a consumer sends to a run while it works, such as what the user typed meanwhile and\n * asked to have read at once. A model-backed agent reads them once the step in progress ends,\n * and wakes for them while it waits for the agents it sent to work in the background. The Agent\n * SDK engines, the orchestrators and the workflows never read them.\n *\n * One inbox belongs to one run: create it for the run, pass it in `RunOptions`, and close it\n * once the run ends.\n */\nexport class Inbox {\n\tconstructor() {\n\t\tstates.set(this, { waiting: [], closed: false, listeners: new Set() });\n\t}\n\n\t/**\n\t * Sends a message to the run. False once the inbox is closed, whatever the text: no run will\n\t * read it.\n\t *\n\t * @throws {RangeError} when the inbox is open and `text` holds nothing but whitespace, which a\n\t * provider may refuse.\n\t */\n\tsend(text: string): boolean {\n\t\tconst state = stateOf(this);\n\t\tif (state.closed) return false;\n\t\tif (text.trim() === '') throw new RangeError('A message to a run needs some text');\n\t\tstate.waiting.push(text);\n\t\tfor (const listener of state.listeners) listener();\n\t\treturn true;\n\t}\n\n\t/**\n\t * Closes the inbox, and returns every message the run did not keep, in the order they were\n\t * sent: those it never read, such as one sent after its final answer, and those it read in an\n\t * attempt that failed. They belong to the next run.\n\t */\n\tclose(): string[] {\n\t\tconst state = stateOf(this);\n\t\tstate.closed = true;\n\t\tstate.listeners.clear();\n\t\treturn state.waiting.splice(0);\n\t}\n}\n\ninterface InboxState {\n\twaiting: string[];\n\tclosed: boolean;\n\tlisteners: Set<() => void>;\n}\n\n// Kept apart from the class, so a consumer sees only `send` and `close`.\nconst states = new WeakMap<Inbox, InboxState>();\n\nfunction stateOf(inbox: Inbox): InboxState {\n\tconst state = states.get(inbox);\n\tif (!state) throw new TypeError('Not an Inbox');\n\treturn state;\n}\n\n/** How a run reads its inbox. Internal. */\nexport interface InboxReader {\n\t/** Takes every message waiting, in the order they were sent. */\n\ttake(): string[];\n\t/** Gives back messages a run read but did not keep, ahead of any sent since. */\n\tgiveBack(messages: readonly string[]): void;\n\t/** Calls `listener` for each message sent, until the returned function is called. */\n\tonMessage(listener: () => void): () => void;\n}\n\n/**\n * How a run reads `inbox`, which comes untyped from a consumer's options.\n *\n * @throws {UnrecoverableError} for anything that is not an `Inbox` of this package, such as a\n * plain object, or one made by another copy of the package.\n */\nexport function readerOf(inbox: unknown): InboxReader {\n\tconst state =\n\t\ttypeof inbox === 'object' && inbox !== null ? states.get(inbox as Inbox) : undefined;\n\tif (!state) {\n\t\tthrow new UnrecoverableError('The run was given an inbox that is not an Inbox', {\n\t\t\tcause: 'RunOptions.inbox, when given, is an Inbox this package made.',\n\t\t});\n\t}\n\treturn {\n\t\ttake: () => state.waiting.splice(0),\n\t\tgiveBack: messages => {\n\t\t\tstate.waiting.unshift(...messages);\n\t\t},\n\t\tonMessage: listener => {\n\t\t\tif (state.closed) return () => undefined;\n\t\t\tstate.listeners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tstate.listeners.delete(listener);\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import { UnrecoverableError } from '#src/shared/domain/errors';
2
+ type Options = ConstructorParameters<typeof UnrecoverableError>[1];
3
+ /**
4
+ * A run started on an agent instance while another run on it works. An instance continues one
5
+ * conversation, so two runs at once would each continue it from the same point, and one would
6
+ * silently drop the other.
7
+ */
8
+ export declare class RunInProgressError extends UnrecoverableError {
9
+ constructor(message?: string, options?: Options);
10
+ }
11
+ /** Lets an agent instance work on one run at a time, and rejects a run that starts meanwhile. */
12
+ export declare class OneRunAtATime {
13
+ private running;
14
+ /** Runs `work`, unless a run is already in progress. */
15
+ run<T>(work: () => Promise<T>): Promise<T>;
16
+ }
17
+ export {};
@@ -0,0 +1,32 @@
1
+ import { UnrecoverableError } from '#src/shared/domain/errors';
2
+ /**
3
+ * A run started on an agent instance while another run on it works. An instance continues one
4
+ * conversation, so two runs at once would each continue it from the same point, and one would
5
+ * silently drop the other.
6
+ */
7
+ export class RunInProgressError extends UnrecoverableError {
8
+ // The parameters keep the shape `withSpentTokens` copies a classified failure with.
9
+ constructor(message = 'The agent already has a run in progress', options = {
10
+ cause: 'An agent instance continues one conversation, one run at a time. Wait for the run in progress to end, or send the message to it through its inbox.',
11
+ }) {
12
+ super(message, options);
13
+ this.name = 'RunInProgressError';
14
+ }
15
+ }
16
+ /** Lets an agent instance work on one run at a time, and rejects a run that starts meanwhile. */
17
+ export class OneRunAtATime {
18
+ running = false;
19
+ /** Runs `work`, unless a run is already in progress. */
20
+ async run(work) {
21
+ if (this.running)
22
+ throw new RunInProgressError();
23
+ this.running = true;
24
+ try {
25
+ return await work();
26
+ }
27
+ finally {
28
+ this.running = false;
29
+ }
30
+ }
31
+ }
32
+ //# sourceMappingURL=oneRunAtATime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oneRunAtATime.js","sourceRoot":"","sources":["../../../src/agent/domain/oneRunAtATime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAI/D;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,kBAAkB;IACzD,oFAAoF;IACpF,YACC,OAAO,GAAG,yCAAyC,EACnD,UAAmB;QAClB,KAAK,EACJ,oJAAoJ;KACrJ;QAED,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IAClC,CAAC;CACD;AAED,iGAAiG;AACjG,MAAM,OAAO,aAAa;IACjB,OAAO,GAAG,KAAK,CAAC;IAExB,wDAAwD;IACxD,KAAK,CAAC,GAAG,CAAI,IAAsB;QAClC,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,kBAAkB,EAAE,CAAC;QACjD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC;YACJ,OAAO,MAAM,IAAI,EAAE,CAAC;QACrB,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACtB,CAAC;IACF,CAAC;CACD","sourcesContent":["import { UnrecoverableError } from '#src/shared/domain/errors';\n\ntype Options = ConstructorParameters<typeof UnrecoverableError>[1];\n\n/**\n * A run started on an agent instance while another run on it works. An instance continues one\n * conversation, so two runs at once would each continue it from the same point, and one would\n * silently drop the other.\n */\nexport class RunInProgressError extends UnrecoverableError {\n\t// The parameters keep the shape `withSpentTokens` copies a classified failure with.\n\tconstructor(\n\t\tmessage = 'The agent already has a run in progress',\n\t\toptions: Options = {\n\t\t\tcause:\n\t\t\t\t'An agent instance continues one conversation, one run at a time. Wait for the run in progress to end, or send the message to it through its inbox.',\n\t\t},\n\t) {\n\t\tsuper(message, options);\n\t\tthis.name = 'RunInProgressError';\n\t}\n}\n\n/** Lets an agent instance work on one run at a time, and rejects a run that starts meanwhile. */\nexport class OneRunAtATime {\n\tprivate running = false;\n\n\t/** Runs `work`, unless a run is already in progress. */\n\tasync run<T>(work: () => Promise<T>): Promise<T> {\n\t\tif (this.running) throw new RunInProgressError();\n\t\tthis.running = true;\n\t\ttry {\n\t\t\treturn await work();\n\t\t} finally {\n\t\t\tthis.running = false;\n\t\t}\n\t}\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { RunOptions } from './agent.ts';
2
+ import type { Session } from '#src/shared/domain/session';
2
3
  export type RunEnd = 'completed' | 'failed' | 'cancelled';
3
4
  /**
4
5
  * What one top-level run shares with every agent and tool inside it, across all its roles,
@@ -10,6 +11,8 @@ export type RunEnd = 'completed' | 'failed' | 'cancelled';
10
11
  */
11
12
  export declare class RunContext {
12
13
  private readonly finishers;
14
+ /** The session the run belongs to, which every agent inside it answers with. */
15
+ readonly session: Session;
13
16
  /**
14
17
  * The run the workspace's history recorded for this run, once it first wrote. The object is
15
18
  * shared, not copied: a turn recorded before the first write still learns its run.
@@ -17,6 +20,7 @@ export declare class RunContext {
17
20
  readonly historyRun: {
18
21
  runId?: string;
19
22
  };
23
+ constructor(session: Session);
20
24
  /** Ends something the run started, when the run ends. */
21
25
  onFinish(finisher: (end: RunEnd) => Promise<void>): void;
22
26
  /**
@@ -8,11 +8,16 @@
8
8
  */
9
9
  export class RunContext {
10
10
  finishers = [];
11
+ /** The session the run belongs to, which every agent inside it answers with. */
12
+ session;
11
13
  /**
12
14
  * The run the workspace's history recorded for this run, once it first wrote. The object is
13
15
  * shared, not copied: a turn recorded before the first write still learns its run.
14
16
  */
15
17
  historyRun = {};
18
+ constructor(session) {
19
+ this.session = session;
20
+ }
16
21
  /** Ends something the run started, when the run ends. */
17
22
  onFinish(finisher) {
18
23
  this.finishers.push(finisher);
@@ -1 +1 @@
1
- {"version":3,"file":"runContext.js","sourceRoot":"","sources":["../../../src/agent/domain/runContext.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,MAAM,OAAO,UAAU;IACL,SAAS,GAAuC,EAAE,CAAC;IAEpE;;;OAGG;IACM,UAAU,GAAuB,EAAE,CAAC;IAE7C,yDAAyD;IACzD,QAAQ,CAAC,QAAwC;QAChD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QACvB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC;gBACJ,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;QACF,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;CACD;AAED,MAAM,aAAa,GAAG,MAAM,CAAC,2BAA2B,CAAC,CAAC;AAM1D,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,OAAmB;IAC/C,OAAQ,OAAgC,CAAC,aAAa,CAAC,CAAC;AACzD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,OAAmB,EAAE,OAAmB;IACtE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,OAAO,EAA0B,CAAC;AACzE,CAAC;AAgBD,MAAM,aAAa,GAAG,MAAM,CAAC,2BAA2B,CAAC,CAAC;AAM1D,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,OAAmB;IAC/C,OAAQ,OAA+B,CAAC,aAAa,CAAC,CAAC;AACxD,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,cAAc,CAAC,OAAmB,EAAE,UAAsB;IACzE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,UAAU,EAAyB,CAAC;AAC3E,CAAC","sourcesContent":["import type { RunOptions } from './agent.ts';\n\nexport type RunEnd = 'completed' | 'failed' | 'cancelled';\n\n/**\n * What one top-level run shares with every agent and tool inside it, across all its roles,\n * rounds and retries, such as the record of what it wrote. Whatever a tool starts for the run\n * registers how to end it, and the agent that created the context ends it with the run.\n *\n * Internal: it travels in `RunOptions` under a symbol, so decorators, which pass the options on\n * whole, carry it without knowing it exists, and no consumer can see or set it.\n */\nexport class RunContext {\n\tprivate readonly finishers: ((end: RunEnd) => Promise<void>)[] = [];\n\n\t/**\n\t * The run the workspace's history recorded for this run, once it first wrote. The object is\n\t * shared, not copied: a turn recorded before the first write still learns its run.\n\t */\n\treadonly historyRun: { runId?: string } = {};\n\n\t/** Ends something the run started, when the run ends. */\n\tonFinish(finisher: (end: RunEnd) => Promise<void>): void {\n\t\tthis.finishers.push(finisher);\n\t}\n\n\t/**\n\t * Ends everything registered, all of it even when one fails, and then throws the first\n\t * failure.\n\t */\n\tasync finish(end: RunEnd): Promise<void> {\n\t\tconst failures: unknown[] = [];\n\t\tfor (const finisher of this.finishers.splice(0)) {\n\t\t\ttry {\n\t\t\t\tawait finisher(end);\n\t\t\t} catch (error) {\n\t\t\t\tfailures.push(error);\n\t\t\t}\n\t\t}\n\t\tif (failures.length > 0) throw failures[0];\n\t}\n}\n\nconst runContextKey = Symbol('mikode-harness.runContext');\n\ninterface ContextualRunOptions extends RunOptions {\n\t[runContextKey]?: RunContext;\n}\n\n/** The context of the run these options belong to, if an outer agent created one. */\nexport function runContextOf(options: RunOptions): RunContext | undefined {\n\treturn (options as ContextualRunOptions)[runContextKey];\n}\n\n/** The same options, carrying `context` for every agent the run reaches. */\nexport function withRunContext(options: RunOptions, context: RunContext): RunOptions {\n\treturn { ...options, [runContextKey]: context } as ContextualRunOptions;\n}\n\n/**\n * Where an agent another agent's tool started runs: the call that started it, and how many\n * delegations deep it is, 1 for an agent the top-level agent started.\n */\nexport interface Delegation {\n\t/** The name of the tool that started it, such as `explore` or a specialist's. */\n\treadonly tool: string;\n\t/** The `id` of that call's `tool` events. */\n\treadonly callId: string;\n\t/** Which of the agents that call started, from 1. */\n\treadonly subagent: number;\n\treadonly depth: number;\n}\n\nconst delegationKey = Symbol('mikode-harness.delegation');\n\ninterface DelegatedRunOptions extends RunOptions {\n\t[delegationKey]?: Delegation;\n}\n\n/** The call that started the agent these options reach, if another agent's tool did. */\nexport function delegationOf(options: RunOptions): Delegation | undefined {\n\treturn (options as DelegatedRunOptions)[delegationKey];\n}\n\n/** The same options, for an agent `delegation` started. */\nexport function withDelegation(options: RunOptions, delegation: Delegation): RunOptions {\n\treturn { ...options, [delegationKey]: delegation } as DelegatedRunOptions;\n}\n"]}
1
+ {"version":3,"file":"runContext.js","sourceRoot":"","sources":["../../../src/agent/domain/runContext.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,MAAM,OAAO,UAAU;IACL,SAAS,GAAuC,EAAE,CAAC;IAEpE,gFAAgF;IACvE,OAAO,CAAU;IAE1B;;;OAGG;IACM,UAAU,GAAuB,EAAE,CAAC;IAE7C,YAAY,OAAgB;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;IAED,yDAAyD;IACzD,QAAQ,CAAC,QAAwC;QAChD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QACvB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC;gBACJ,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;QACF,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;CACD;AAED,MAAM,aAAa,GAAG,MAAM,CAAC,2BAA2B,CAAC,CAAC;AAM1D,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,OAAmB;IAC/C,OAAQ,OAAgC,CAAC,aAAa,CAAC,CAAC;AACzD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,OAAmB,EAAE,OAAmB;IACtE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,OAAO,EAA0B,CAAC;AACzE,CAAC;AAgBD,MAAM,aAAa,GAAG,MAAM,CAAC,2BAA2B,CAAC,CAAC;AAM1D,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,OAAmB;IAC/C,OAAQ,OAA+B,CAAC,aAAa,CAAC,CAAC;AACxD,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,cAAc,CAAC,OAAmB,EAAE,UAAsB;IACzE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,UAAU,EAAyB,CAAC;AAC3E,CAAC","sourcesContent":["import type { RunOptions } from './agent.ts';\nimport type { Session } from '#src/shared/domain/session';\n\nexport type RunEnd = 'completed' | 'failed' | 'cancelled';\n\n/**\n * What one top-level run shares with every agent and tool inside it, across all its roles,\n * rounds and retries, such as the record of what it wrote. Whatever a tool starts for the run\n * registers how to end it, and the agent that created the context ends it with the run.\n *\n * Internal: it travels in `RunOptions` under a symbol, so decorators, which pass the options on\n * whole, carry it without knowing it exists, and no consumer can see or set it.\n */\nexport class RunContext {\n\tprivate readonly finishers: ((end: RunEnd) => Promise<void>)[] = [];\n\n\t/** The session the run belongs to, which every agent inside it answers with. */\n\treadonly session: Session;\n\n\t/**\n\t * The run the workspace's history recorded for this run, once it first wrote. The object is\n\t * shared, not copied: a turn recorded before the first write still learns its run.\n\t */\n\treadonly historyRun: { runId?: string } = {};\n\n\tconstructor(session: Session) {\n\t\tthis.session = session;\n\t}\n\n\t/** Ends something the run started, when the run ends. */\n\tonFinish(finisher: (end: RunEnd) => Promise<void>): void {\n\t\tthis.finishers.push(finisher);\n\t}\n\n\t/**\n\t * Ends everything registered, all of it even when one fails, and then throws the first\n\t * failure.\n\t */\n\tasync finish(end: RunEnd): Promise<void> {\n\t\tconst failures: unknown[] = [];\n\t\tfor (const finisher of this.finishers.splice(0)) {\n\t\t\ttry {\n\t\t\t\tawait finisher(end);\n\t\t\t} catch (error) {\n\t\t\t\tfailures.push(error);\n\t\t\t}\n\t\t}\n\t\tif (failures.length > 0) throw failures[0];\n\t}\n}\n\nconst runContextKey = Symbol('mikode-harness.runContext');\n\ninterface ContextualRunOptions extends RunOptions {\n\t[runContextKey]?: RunContext;\n}\n\n/** The context of the run these options belong to, if an outer agent created one. */\nexport function runContextOf(options: RunOptions): RunContext | undefined {\n\treturn (options as ContextualRunOptions)[runContextKey];\n}\n\n/** The same options, carrying `context` for every agent the run reaches. */\nexport function withRunContext(options: RunOptions, context: RunContext): RunOptions {\n\treturn { ...options, [runContextKey]: context } as ContextualRunOptions;\n}\n\n/**\n * Where an agent another agent's tool started runs: the call that started it, and how many\n * delegations deep it is, 1 for an agent the top-level agent started.\n */\nexport interface Delegation {\n\t/** The name of the tool that started it, such as `explore` or a specialist's. */\n\treadonly tool: string;\n\t/** The `id` of that call's `tool` events. */\n\treadonly callId: string;\n\t/** Which of the agents that call started, from 1. */\n\treadonly subagent: number;\n\treadonly depth: number;\n}\n\nconst delegationKey = Symbol('mikode-harness.delegation');\n\ninterface DelegatedRunOptions extends RunOptions {\n\t[delegationKey]?: Delegation;\n}\n\n/** The call that started the agent these options reach, if another agent's tool did. */\nexport function delegationOf(options: RunOptions): Delegation | undefined {\n\treturn (options as DelegatedRunOptions)[delegationKey];\n}\n\n/** The same options, for an agent `delegation` started. */\nexport function withDelegation(options: RunOptions, delegation: Delegation): RunOptions {\n\treturn { ...options, [delegationKey]: delegation } as DelegatedRunOptions;\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { RunOptions } from './agent.ts';
2
+ import type { Session } from '#src/shared/domain/session';
3
+ /**
4
+ * A session of its own, for an agent the consumer gave none: its id, such as `smart-hawk-4821`,
5
+ * is easier to read and say than a UUID, and unlikely to repeat in one workspace's history.
6
+ */
7
+ export declare function newSession(): Session;
8
+ /**
9
+ * The session a run belongs to: the outer run's, for an agent another agent started, or the one
10
+ * its options name. Absent when neither gives one, and the agent uses its own.
11
+ *
12
+ * @throws {UnrecoverableError} when the options name a session that is not an object, has no
13
+ * id, or has a name that is not text.
14
+ */
15
+ export declare function sessionOf(options: RunOptions): Session | undefined;