@mikode13/harness 9.3.0 → 10.0.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 (73) hide show
  1. package/README.md +66 -8
  2. package/dist/agent/domain/agent.d.ts +11 -5
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/approval.d.ts +2 -1
  5. package/dist/agent/domain/approval.js +2 -1
  6. package/dist/agent/domain/approval.js.map +1 -1
  7. package/dist/agent/domain/question.d.ts +5 -0
  8. package/dist/agent/domain/question.js +11 -0
  9. package/dist/agent/domain/question.js.map +1 -1
  10. package/dist/agent/domain/runContext.d.ts +17 -0
  11. package/dist/agent/domain/runContext.js +9 -0
  12. package/dist/agent/domain/runContext.js.map +1 -1
  13. package/dist/agent/domain/transcript.d.ts +10 -0
  14. package/dist/agent/domain/transcript.js.map +1 -1
  15. package/dist/engines/domain/model/askUser.d.ts +0 -5
  16. package/dist/engines/domain/model/askUser.js +0 -9
  17. package/dist/engines/domain/model/askUser.js.map +1 -1
  18. package/dist/engines/domain/model/historyFollower.d.ts +1 -1
  19. package/dist/engines/domain/model/historyFollower.js +14 -3
  20. package/dist/engines/domain/model/historyFollower.js.map +1 -1
  21. package/dist/engines/domain/model/llmAgent.d.ts +19 -3
  22. package/dist/engines/domain/model/llmAgent.js +141 -51
  23. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  24. package/dist/factory/infrastructure/agentLLMFactory.d.ts +6 -2
  25. package/dist/factory/infrastructure/agentLLMFactory.js +4 -4
  26. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/recovery/domain/history.d.ts +3 -2
  31. package/dist/recovery/domain/history.js +6 -2
  32. package/dist/recovery/domain/history.js.map +1 -1
  33. package/dist/recovery/domain/recoveryStore.d.ts +47 -5
  34. package/dist/recovery/domain/recoveryStore.js.map +1 -1
  35. package/dist/recovery/domain/runChanges.js +11 -2
  36. package/dist/recovery/domain/runChanges.js.map +1 -1
  37. package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +41 -9
  38. package/dist/recovery/infrastructure/fileRecoveryStore.js +217 -65
  39. package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
  40. package/dist/tools/domain/preparedCall.d.ts +14 -0
  41. package/dist/tools/domain/preparedCall.js +12 -0
  42. package/dist/tools/domain/preparedCall.js.map +1 -1
  43. package/dist/tools/domain/readRegistry.d.ts +7 -0
  44. package/dist/tools/domain/readRegistry.js.map +1 -1
  45. package/dist/tools/infrastructure/agentAsTool.d.ts +61 -0
  46. package/dist/tools/infrastructure/agentAsTool.js +144 -0
  47. package/dist/tools/infrastructure/agentAsTool.js.map +1 -0
  48. package/dist/tools/infrastructure/commandCheckpoint.d.ts +3 -2
  49. package/dist/tools/infrastructure/commandCheckpoint.js +21 -9
  50. package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -1
  51. package/dist/tools/infrastructure/commandTool.js +7 -3
  52. package/dist/tools/infrastructure/commandTool.js.map +1 -1
  53. package/dist/tools/infrastructure/exploreTool.js +9 -22
  54. package/dist/tools/infrastructure/exploreTool.js.map +1 -1
  55. package/dist/tools/infrastructure/fileEditor.d.ts +2 -0
  56. package/dist/tools/infrastructure/fileEditor.js +5 -1
  57. package/dist/tools/infrastructure/fileEditor.js.map +1 -1
  58. package/dist/tools/infrastructure/fileFormatter.d.ts +1 -0
  59. package/dist/tools/infrastructure/fileFormatter.js +5 -1
  60. package/dist/tools/infrastructure/fileFormatter.js.map +1 -1
  61. package/dist/tools/infrastructure/fileTools.d.ts +1 -1
  62. package/dist/tools/infrastructure/fileTools.js +3 -3
  63. package/dist/tools/infrastructure/fileTools.js.map +1 -1
  64. package/dist/tools/infrastructure/subagents.d.ts +19 -0
  65. package/dist/tools/infrastructure/subagents.js +29 -0
  66. package/dist/tools/infrastructure/subagents.js.map +1 -0
  67. package/dist/tools/infrastructure/workspaceWrites.d.ts +8 -5
  68. package/dist/tools/infrastructure/workspaceWrites.js +19 -7
  69. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
  70. package/dist/tools/infrastructure/writeSession.d.ts +8 -0
  71. package/dist/tools/infrastructure/writeSession.js +37 -0
  72. package/dist/tools/infrastructure/writeSession.js.map +1 -1
  73. package/package.json +1 -1
package/README.md CHANGED
@@ -43,7 +43,9 @@ chat loop itself never knows the difference.
43
43
  harness ships tools that read the repository (list, search, read), bounded by
44
44
  `.gitignore`, and `createFileTools`, which also change files in each provider's
45
45
  native format, every change recorded so it can be undone, and run the commands you
46
- configure, such as the tests, recording what each one changed too.
46
+ configure, such as the tests, recording what each one changed too. `agentAsTool` offers
47
+ one agent to another as a tool, such as a cheaper committer to a coding agent, working
48
+ inside the caller's run.
47
49
  - **`OrchestratorAgent`**: coordinates a planner, an executor, and a reviewer
48
50
  (each an injected `Agent`) in a plan → execute → review loop. The reviewer's
49
51
  decision is a Zod-validated structured `{decision, feedback}`, not free text —
@@ -495,10 +497,12 @@ run in `AgentResponse.runId`. Progress reports each change as a unified `diff` o
495
497
 
496
498
  For debugging, `onTranscript` receives what the model saw in each run once the run ended,
497
499
  completed, failed or cancelled. A `Transcript` has the run's `outcome`, the role it was of in
498
- `createLLMOrchestrator`, or `explorer`, and a `content` of plain JSON: the instructions, the tool
499
- definitions, the run's messages with what the tools returned, and the error a run that did not
500
- complete ended with. `content` has no stable shape; read it, but do not build on it. A callback
501
- that fails is logged, and the run stays as it ended.
500
+ `createLLMOrchestrator`, or `explorer`, and a `content` of plain JSON: the instructions, the
501
+ tool definitions, the run's messages with what the tools returned, and the error a run that did
502
+ not complete ended with. The transcript of an agent another agent handed a task to also has
503
+ `delegation`: the tool, the call's id and the subagent's number that started it. `content`
504
+ has no stable shape; read it, but do not build on it. A callback that fails is logged, and the
505
+ run stays as it ended.
502
506
 
503
507
  `createLLMOrchestrator()` takes the options of `createOrchestrator()` plus `workspace`,
504
508
  `maxSteps`, `compactAt`, `commands`, `formatter`, `summarizeUndone` and `onTranscript`, and returns a promise. Every role runs on the
@@ -550,6 +554,56 @@ createLLMOrchestrator({ workspace, compactAt: { executor: 200_000 } });
550
554
  Without `workspace` it works on the current directory, read only. One run of the
551
555
  orchestrator is one run of the history, every round included.
552
556
 
557
+ ### Agents that hand work to other agents
558
+
559
+ `agentAsTool` offers an agent to another one as a tool. The calling model writes the task, and
560
+ a new agent from the `createAgent` option runs it; its answer is the call's result. Specialists can come
561
+ from a package you reuse in every project, or be written in one project:
562
+
563
+ ```ts
564
+ import { agentAsTool, createFileTools, createLLMAgent } from '@mikode13/harness';
565
+
566
+ const tools = await createFileTools('anthropic', workspace, { commands });
567
+ const commit = agentAsTool({
568
+ name: 'commit',
569
+ description: 'Commits the work done so far, with a message that says why.',
570
+ createAgent: () =>
571
+ createLLMAgent('anthropic', {
572
+ model: 'haiku',
573
+ systemPrompt: 'You commit the changes you are told about, one commit, with a clear message.',
574
+ tools,
575
+ }),
576
+ });
577
+ const coder = createLLMAgent('anthropic', {
578
+ systemPrompt: 'You write code, with a test for each change, and commit when the tests pass.',
579
+ tools: [...tools, commit],
580
+ workspace,
581
+ });
582
+ ```
583
+
584
+ The agent works inside the caller's run:
585
+
586
+ - what it writes is the same run of the history, so one undo reverts both; it keeps its own
587
+ tools' access policy, and edits only what it read itself;
588
+ - what it spends counts in the caller's `tokens`, a failed attempt included;
589
+ - it asks and is approved through the run's `ask` and `approve`, which agents working at once
590
+ may call before an earlier call has its answer; one person at one terminal needs them queued,
591
+ as the CLI does. Delegating never grants `autoApprove`; the agent has it only if the
592
+ `createAgent` option gave it. Handing over a task asks no one unless you set
593
+ `risk: 'destructive'`;
594
+ - its progress arrives as `subagent` events of the call, with the tool's `name`, and its
595
+ transcript names the call in `delegation`.
596
+
597
+ Calls to these tools next to each other in one answer run at once; a call to another tool
598
+ between them waits for those before it, and the ones after it wait for it. The history stays
599
+ exact, because every change and command of a run is made one at a time, but agents working at
600
+ once can see each other's unfinished work, and a long command holds the others' writes until it
601
+ ends. An agent that fails
602
+ is the call's error, for the model to act on; a cancellation, or your approver or asker failing
603
+ inside it, ends the run. A call deeper than `maxDepth`, 2 by default, is refused. Only agents
604
+ from `createLLMAgent` work this way: an Agent SDK agent from the `createAgent` factory runs
605
+ as a run of its own.
606
+
553
607
  ### Running the project's commands
554
608
 
555
609
  With `commands`, `createFileTools` and `createLLMOrchestrator` add `runCommand`, which runs
@@ -642,7 +696,9 @@ writing is still listed, with that status. A run that changed nothing is not kep
642
696
 
643
697
  A move writes a file only if it still holds what the history expects. A file you
644
698
  changed since is left as it is and named in the move's `conflicts`, and the move is
645
- then not `complete`. The reason is kept with the move.
699
+ then not `complete`. So is a file a command changed into, or from, something the history
700
+ cannot copy, such as a file over 64 MB: what it held on the other side is unknown. The
701
+ reason is kept with the move.
646
702
 
647
703
  A move can be tied to where the workspace is. Pass `from`, a run or `historyStart`, and
648
704
  the move happens only if the workspace is still there; otherwise it fails with
@@ -748,8 +804,10 @@ heading. Reasoning is dim italic and complete by default; pass
748
804
  updates the spinner rather than printing a second line, and cancelling a turn clears
749
805
  its pending spinner state. Pass `--steps <file>` to append one JSON line per call to the
750
806
  model to that file: the call's tokens and what its context held, by origin, for measuring
751
- what a run spends. An explorer's call is written as the `subagent` event that carries it,
752
- which names the role that sent the explorer and its number. Pass `--debug <folder>` to keep,
807
+ what a run spends. A subagent's call is written as the `subagent` events that carry it, which
808
+ name the role and the agents that led to it. Each subagent's calls and messages are shown as a
809
+ thread, in a color of its own, such as `planner › explorer 2`, and questions and approvals of
810
+ agents working at once take turns. Pass `--debug <folder>` to keep,
753
811
  in a folder for the session under it, every progress event in `events.jsonl` and, with
754
812
  `--llm`, the transcript of every run of every agent, one numbered file each, such as
755
813
  `003-executor-failed.json`. A failed tool call's event says what the model was told, in
@@ -40,13 +40,15 @@ export interface RunOptions {
40
40
  /**
41
41
  * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call
42
42
  * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore
43
- * it: their own permission systems decide.
43
+ * it: their own permission systems decide. Agents a delegation runs at once may call it before
44
+ * an earlier call has its answer.
44
45
  */
45
46
  approve?: Approver | undefined;
46
47
  /**
47
48
  * Answers what a model-backed agent asks the user, through its `askUser` tool, when a
48
49
  * decision is the user's to make. Without it the agent has no such tool. The Agent SDK
49
- * engines ignore it.
50
+ * engines ignore it. Agents a delegation runs at once may call it before an earlier call has
51
+ * its answer.
50
52
  */
51
53
  ask?: Asker | undefined;
52
54
  }
@@ -163,14 +165,18 @@ export type ProgressEvent = {
163
165
  agent?: ProgressAgent;
164
166
  }
165
167
  /**
166
- * An event of a subagent that a tool call started, such as one of the explorers an `explore`
167
- * call runs at once. `callId` is the `id` of that call's `tool` events, and `subagent`
168
- * numbers its subagents from 1, so the events of agents running in parallel stay apart.
168
+ * An event of a subagent that a tool call started: one of the explorers an `explore` call runs
169
+ * at once, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that
170
+ * call's `tool` events, and `subagent` numbers its subagents from 1, so the events of agents
171
+ * running in parallel stay apart. A subagent's own subagents arrive as `subagent` events
172
+ * inside this one's `event`.
169
173
  */
170
174
  | {
171
175
  type: 'subagent';
172
176
  callId: string;
173
177
  subagent: number;
178
+ /** Which agent it is: `explorer`, or the name of the tool that delegates to it. */
179
+ name: string;
174
180
  /** What the subagent was asked. */
175
181
  task: string;
176
182
  event: ProgressEvent;
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AAwDA,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.\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.\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.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied';\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, such as one of the explorers an `explore`\n\t * call runs at once. `callId` is the `id` of that call's `tool` events, and `subagent`\n\t * numbers its subagents from 1, so the events of agents running in parallel stay apart.\n\t */\n\t| {\n\t\t\ttype: 'subagent';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\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\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":"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.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied';\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 runs\n\t * at once, 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. A subagent's own subagents arrive as `subagent` events\n\t * 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\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"]}
@@ -46,7 +46,8 @@ export type RememberableDecision = {
46
46
  * Wraps `ask` so a call the user allowed with `remember` is not asked about again. `key` decides
47
47
  * what counts as the same call; it defaults to the tool's name, so "always" allows every
48
48
  * destructive call to that tool. The memory lives in the returned approver, so its lifetime is
49
- * the consumer's choice: one per session, one per user, or one per run.
49
+ * the consumer's choice: one per session, one per user, or one per run. Calls made at once are
50
+ * each asked; a call that waits its turn in front of it finds an "always" given meanwhile.
50
51
  */
51
52
  export declare function rememberApprovals(ask: (request: ApprovalRequest, signal: AbortSignal) => RememberableDecision | Promise<RememberableDecision>, { key }?: {
52
53
  key?: (request: ApprovalRequest) => string;
@@ -2,7 +2,8 @@
2
2
  * Wraps `ask` so a call the user allowed with `remember` is not asked about again. `key` decides
3
3
  * what counts as the same call; it defaults to the tool's name, so "always" allows every
4
4
  * destructive call to that tool. The memory lives in the returned approver, so its lifetime is
5
- * the consumer's choice: one per session, one per user, or one per run.
5
+ * the consumer's choice: one per session, one per user, or one per run. Calls made at once are
6
+ * each asked; a call that waits its turn in front of it finds an "always" given meanwhile.
6
7
  */
7
8
  export function rememberApprovals(ask, { key = request => request.tool } = {}) {
8
9
  const allowed = new Set();
@@ -1 +1 @@
1
- {"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"AA6CA;;;;;GAKG;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.\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":"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"]}
@@ -17,6 +17,11 @@ export interface UserQuestion {
17
17
  * `UnrecoverableError`, as a failing approver does.
18
18
  */
19
19
  export type Asker = (questions: UserQuestion[], signal: AbortSignal) => string[] | Promise<string[]>;
20
+ /**
21
+ * The asker's answers, one non-empty string per question, or nothing: an asker is host code, so
22
+ * an answer of any other shape is its bug, not the model's.
23
+ */
24
+ export declare function readAnswers(answers: unknown, questions: UserQuestion[]): string[] | undefined;
20
25
  /** What one run's user answered, as `rememberAnswers` keeps it. */
21
26
  export interface AnsweredQuestion {
22
27
  question: string;
@@ -1,3 +1,14 @@
1
+ /**
2
+ * The asker's answers, one non-empty string per question, or nothing: an asker is host code, so
3
+ * an answer of any other shape is its bug, not the model's.
4
+ */
5
+ export function readAnswers(answers, questions) {
6
+ if (!Array.isArray(answers) || answers.length !== questions.length)
7
+ return undefined;
8
+ return answers.every(answer => typeof answer === 'string' && answer.trim() !== '')
9
+ ? answers
10
+ : undefined;
11
+ }
1
12
  /**
2
13
  * Wraps `ask` so a question asked again with the same words gets the answer the user already
3
14
  * gave, and only the others are asked. `answered` holds every answer, in the order given. The
@@ -1 +1 @@
1
- {"version":3,"file":"question.js","sourceRoot":"","sources":["../../../src/agent/domain/question.ts"],"names":[],"mappings":"AA+BA;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAU;IACzC,MAAM,QAAQ,GAAuB,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IAExC,OAAO;QACN,QAAQ;QACR,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE;YAChC,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;YACzE,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACnE,8EAA8E;YAC9E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAC;YACxC,KAAK,MAAM,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;gBACvD,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC5B,IAAI,OAAO,MAAM,KAAK,QAAQ;oBAAE,OAAO,KAAK,CAAC;gBAC7C,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;gBAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YACrC,CAAC;YACD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACnE,CAAC;KACD,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,gBAAgB,CAAC,QAA4B;IAC5D,OAAO;QACN,yFAAyF;QACzF,GAAG,QAAQ,CAAC,GAAG,CACd,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,QAAQ,QAAQ,MAAM,EAAE,CAClF;KACD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC","sourcesContent":["/** One choice the model offers for a question. */\nexport interface QuestionOption {\n\tlabel: string;\n\tdescription: string;\n}\n\n/** A decision the model leaves to the user, with the choices it sees. */\nexport interface UserQuestion {\n\tquestion: string;\n\t/** Two to four. The user may always answer in their own words instead. */\n\toptions: QuestionOption[];\n\t/** Whether more than one option may be chosen. */\n\tmultiSelect: boolean;\n}\n\n/**\n * Answers the questions a model-backed agent asks, one answer per question and in order: the\n * label of each option chosen, or the user's own words. A throw ends the run as an\n * `UnrecoverableError`, as a failing approver does.\n */\nexport type Asker = (\n\tquestions: UserQuestion[],\n\tsignal: AbortSignal,\n) => string[] | Promise<string[]>;\n\n/** What one run's user answered, as `rememberAnswers` keeps it. */\nexport interface AnsweredQuestion {\n\tquestion: string;\n\tanswer: string;\n}\n\n/**\n * Wraps `ask` so a question asked again with the same words gets the answer the user already\n * gave, and only the others are asked. `answered` holds every answer, in the order given. The\n * memory lives as long as the returned asker: one per run, so a retry never asks again.\n */\nexport function rememberAnswers(ask: Asker): { ask: Asker; answered: AnsweredQuestion[] } {\n\tconst answered: AnsweredQuestion[] = [];\n\tconst known = new Map<string, string>();\n\n\treturn {\n\t\tanswered,\n\t\task: async (questions, signal) => {\n\t\t\tconst unknown = questions.filter(({ question }) => !known.has(question));\n\t\t\tconst fresh = unknown.length > 0 ? await ask(unknown, signal) : [];\n\t\t\t// The agent checks the asker's answers; a short or odd list is its to refuse.\n\t\t\tif (!Array.isArray(fresh)) return fresh;\n\t\t\tfor (const [index, { question }] of unknown.entries()) {\n\t\t\t\tconst answer = fresh[index];\n\t\t\t\tif (typeof answer !== 'string') return fresh;\n\t\t\t\tknown.set(question, answer);\n\t\t\t\tanswered.push({ question, answer });\n\t\t\t}\n\t\t\treturn questions.map(({ question }) => known.get(question) ?? '');\n\t\t},\n\t};\n}\n\n/** The user's earlier answers, for a prompt that must not ask them again. */\nexport function describeAnswered(answered: AnsweredQuestion[]): string {\n\treturn [\n\t\t'The user already answered these questions in an earlier attempt; do not ask them again:',\n\t\t...answered.map(\n\t\t\t({ question, answer }, index) => `${String(index + 1)}. ${question}\\n ${answer}`,\n\t\t),\n\t].join('\\n');\n}\n"]}
1
+ {"version":3,"file":"question.js","sourceRoot":"","sources":["../../../src/agent/domain/question.ts"],"names":[],"mappings":"AAyBA;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,OAAgB,EAAE,SAAyB;IACtE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACrF,OAAO,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QACjF,CAAC,CAAE,OAAoB;QACvB,CAAC,CAAC,SAAS,CAAC;AACd,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAU;IACzC,MAAM,QAAQ,GAAuB,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IAExC,OAAO;QACN,QAAQ;QACR,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE;YAChC,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;YACzE,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACnE,8EAA8E;YAC9E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAC;YACxC,KAAK,MAAM,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;gBACvD,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC5B,IAAI,OAAO,MAAM,KAAK,QAAQ;oBAAE,OAAO,KAAK,CAAC;gBAC7C,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;gBAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YACrC,CAAC;YACD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACnE,CAAC;KACD,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,gBAAgB,CAAC,QAA4B;IAC5D,OAAO;QACN,yFAAyF;QACzF,GAAG,QAAQ,CAAC,GAAG,CACd,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,QAAQ,QAAQ,MAAM,EAAE,CAClF;KACD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC","sourcesContent":["/** One choice the model offers for a question. */\nexport interface QuestionOption {\n\tlabel: string;\n\tdescription: string;\n}\n\n/** A decision the model leaves to the user, with the choices it sees. */\nexport interface UserQuestion {\n\tquestion: string;\n\t/** Two to four. The user may always answer in their own words instead. */\n\toptions: QuestionOption[];\n\t/** Whether more than one option may be chosen. */\n\tmultiSelect: boolean;\n}\n\n/**\n * Answers the questions a model-backed agent asks, one answer per question and in order: the\n * label of each option chosen, or the user's own words. A throw ends the run as an\n * `UnrecoverableError`, as a failing approver does.\n */\nexport type Asker = (\n\tquestions: UserQuestion[],\n\tsignal: AbortSignal,\n) => string[] | Promise<string[]>;\n\n/**\n * The asker's answers, one non-empty string per question, or nothing: an asker is host code, so\n * an answer of any other shape is its bug, not the model's.\n */\nexport function readAnswers(answers: unknown, questions: UserQuestion[]): string[] | undefined {\n\tif (!Array.isArray(answers) || answers.length !== questions.length) return undefined;\n\treturn answers.every(answer => typeof answer === 'string' && answer.trim() !== '')\n\t\t? (answers as string[])\n\t\t: undefined;\n}\n\n/** What one run's user answered, as `rememberAnswers` keeps it. */\nexport interface AnsweredQuestion {\n\tquestion: string;\n\tanswer: string;\n}\n\n/**\n * Wraps `ask` so a question asked again with the same words gets the answer the user already\n * gave, and only the others are asked. `answered` holds every answer, in the order given. The\n * memory lives as long as the returned asker: one per run, so a retry never asks again.\n */\nexport function rememberAnswers(ask: Asker): { ask: Asker; answered: AnsweredQuestion[] } {\n\tconst answered: AnsweredQuestion[] = [];\n\tconst known = new Map<string, string>();\n\n\treturn {\n\t\tanswered,\n\t\task: async (questions, signal) => {\n\t\t\tconst unknown = questions.filter(({ question }) => !known.has(question));\n\t\t\tconst fresh = unknown.length > 0 ? await ask(unknown, signal) : [];\n\t\t\t// The agent checks the asker's answers; a short or odd list is its to refuse.\n\t\t\tif (!Array.isArray(fresh)) return fresh;\n\t\t\tfor (const [index, { question }] of unknown.entries()) {\n\t\t\t\tconst answer = fresh[index];\n\t\t\t\tif (typeof answer !== 'string') return fresh;\n\t\t\t\tknown.set(question, answer);\n\t\t\t\tanswered.push({ question, answer });\n\t\t\t}\n\t\t\treturn questions.map(({ question }) => known.get(question) ?? '');\n\t\t},\n\t};\n}\n\n/** The user's earlier answers, for a prompt that must not ask them again. */\nexport function describeAnswered(answered: AnsweredQuestion[]): string {\n\treturn [\n\t\t'The user already answered these questions in an earlier attempt; do not ask them again:',\n\t\t...answered.map(\n\t\t\t({ question, answer }, index) => `${String(index + 1)}. ${question}\\n ${answer}`,\n\t\t),\n\t].join('\\n');\n}\n"]}
@@ -29,3 +29,20 @@ export declare class RunContext {
29
29
  export declare function runContextOf(options: RunOptions): RunContext | undefined;
30
30
  /** The same options, carrying `context` for every agent the run reaches. */
31
31
  export declare function withRunContext(options: RunOptions, context: RunContext): RunOptions;
32
+ /**
33
+ * Where an agent another agent's tool started runs: the call that started it, and how many
34
+ * delegations deep it is, 1 for an agent the top-level agent started.
35
+ */
36
+ export interface Delegation {
37
+ /** The name of the tool that started it, such as `explore` or a specialist's. */
38
+ readonly tool: string;
39
+ /** The `id` of that call's `tool` events. */
40
+ readonly callId: string;
41
+ /** Which of the agents that call started, from 1. */
42
+ readonly subagent: number;
43
+ readonly depth: number;
44
+ }
45
+ /** The call that started the agent these options reach, if another agent's tool did. */
46
+ export declare function delegationOf(options: RunOptions): Delegation | undefined;
47
+ /** The same options, for an agent `delegation` started. */
48
+ export declare function withDelegation(options: RunOptions, delegation: Delegation): RunOptions;
@@ -44,4 +44,13 @@ export function runContextOf(options) {
44
44
  export function withRunContext(options, context) {
45
45
  return { ...options, [runContextKey]: context };
46
46
  }
47
+ const delegationKey = Symbol('mikode-harness.delegation');
48
+ /** The call that started the agent these options reach, if another agent's tool did. */
49
+ export function delegationOf(options) {
50
+ return options[delegationKey];
51
+ }
52
+ /** The same options, for an agent `delegation` started. */
53
+ export function withDelegation(options, delegation) {
54
+ return { ...options, [delegationKey]: delegation };
55
+ }
47
56
  //# sourceMappingURL=runContext.js.map
@@ -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","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"]}
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"]}
@@ -9,6 +9,16 @@ export interface Transcript {
9
9
  * sent. Missing for an agent from `createLLMAgent`.
10
10
  */
11
11
  agent?: ProgressAgent | 'explorer';
12
+ /**
13
+ * For an agent another agent's tool started, such as an explorer or the agent of a tool from
14
+ * `agentAsTool`, the call that started it: the tool's name, the `id` of that call, which is
15
+ * in the transcript of the agent that made it, and which of the call's agents it was, from 1.
16
+ */
17
+ delegation?: {
18
+ tool: string;
19
+ callId: string;
20
+ subagent: number;
21
+ };
12
22
  /** How the run ended. A failed run may be retried, and each attempt has its own transcript. */
13
23
  outcome: 'completed' | 'failed' | 'cancelled';
14
24
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"transcript.js","sourceRoot":"","sources":["../../../src/agent/domain/transcript.ts"],"names":[],"mappings":"","sourcesContent":["import type { ProgressAgent } from './agent.ts';\n\n/**\n * What the model of one run saw, for a person debugging a run that went wrong. Only agents on\n * the model APIs produce one.\n */\nexport interface Transcript {\n\t/**\n\t * In `createLLMOrchestrator`, the role the run was of, or `explorer` for an explorer a role\n\t * sent. Missing for an agent from `createLLMAgent`.\n\t */\n\tagent?: ProgressAgent | 'explorer';\n\t/** How the run ended. A failed run may be retried, and each attempt has its own transcript. */\n\toutcome: 'completed' | 'failed' | 'cancelled';\n\t/**\n\t * Plain JSON for a person to read: the instructions, the tool definitions, every message of\n\t * the run's context with the model's last answer and what the tools returned, and the error\n\t * a run that did not complete ended with. Unstable: its shape can change in any release, so\n\t * read it, but do not build on it.\n\t */\n\tcontent: unknown;\n}\n\n/**\n * Receives each run's transcript once the run ended, however it ended. A failure here is\n * logged, never the run's: debugging must not change what it observes.\n */\nexport type TranscriptCallback = (transcript: Transcript) => void | Promise<void>;\n"]}
1
+ {"version":3,"file":"transcript.js","sourceRoot":"","sources":["../../../src/agent/domain/transcript.ts"],"names":[],"mappings":"","sourcesContent":["import type { ProgressAgent } from './agent.ts';\n\n/**\n * What the model of one run saw, for a person debugging a run that went wrong. Only agents on\n * the model APIs produce one.\n */\nexport interface Transcript {\n\t/**\n\t * In `createLLMOrchestrator`, the role the run was of, or `explorer` for an explorer a role\n\t * sent. Missing for an agent from `createLLMAgent`.\n\t */\n\tagent?: ProgressAgent | 'explorer';\n\t/**\n\t * For an agent another agent's tool started, such as an explorer or the agent of a tool from\n\t * `agentAsTool`, the call that started it: the tool's name, the `id` of that call, which is\n\t * in the transcript of the agent that made it, and which of the call's agents it was, from 1.\n\t */\n\tdelegation?: { tool: string; callId: string; subagent: number };\n\t/** How the run ended. A failed run may be retried, and each attempt has its own transcript. */\n\toutcome: 'completed' | 'failed' | 'cancelled';\n\t/**\n\t * Plain JSON for a person to read: the instructions, the tool definitions, every message of\n\t * the run's context with the model's last answer and what the tools returned, and the error\n\t * a run that did not complete ended with. Unstable: its shape can change in any release, so\n\t * read it, but do not build on it.\n\t */\n\tcontent: unknown;\n}\n\n/**\n * Receives each run's transcript once the run ended, however it ended. A failure here is\n * logged, never the run's: debugging must not change what it observes.\n */\nexport type TranscriptCallback = (transcript: Transcript) => void | Promise<void>;\n"]}
@@ -4,11 +4,6 @@ export declare const askUserName = "askUser";
4
4
  export declare const askUserDefinition: ToolDefinition;
5
5
  /** The questions in a call's input, or why they cannot be asked, which the model reads. */
6
6
  export declare function readQuestions(input: unknown): UserQuestion[] | string;
7
- /**
8
- * The asker's answers, one non-empty string per question, or nothing: an asker is host code, so
9
- * an answer of any other shape is its bug, not the model's.
10
- */
11
- export declare function readAnswers(answers: unknown, questions: UserQuestion[]): string[] | undefined;
12
7
  /** What the model reads back: each question with its answer. */
13
8
  export declare function describeAnswers(questions: UserQuestion[], answers: string[]): string;
14
9
  export declare function summarizeQuestions(questions: UserQuestion[]): string;
@@ -79,15 +79,6 @@ export function readQuestions(input) {
79
79
  }
80
80
  return read;
81
81
  }
82
- /**
83
- * The asker's answers, one non-empty string per question, or nothing: an asker is host code, so
84
- * an answer of any other shape is its bug, not the model's.
85
- */
86
- export function readAnswers(answers, questions) {
87
- if (!Array.isArray(answers) || answers.length !== questions.length)
88
- return undefined;
89
- return answers.every(isText) ? answers : undefined;
90
- }
91
82
  /** What the model reads back: each question with its answer. */
92
83
  export function describeAnswers(questions, answers) {
93
84
  return [
@@ -1 +1 @@
1
- {"version":3,"file":"askUser.js","sourceRoot":"","sources":["../../../../src/engines/domain/model/askUser.ts"],"names":[],"mappings":"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","sourcesContent":["import type { UserQuestion } from '#src/agent/domain/question';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\n\nexport const askUserName = 'askUser';\n\nconst maxQuestions = 4;\nconst minOptions = 2;\nconst maxOptions = 4;\n\n// Strict mode allows no length bounds, so the counts live in the description and in `readQuestions`.\nexport const askUserDefinition: ToolDefinition = {\n\tname: askUserName,\n\tdescription: [\n\t\t'Asks the user to decide something that is theirs to decide, and waits for the answer.',\n\t\t'Ask before you build on a choice, not after. The user decides: the shape of a public API or of anything other code depends on; a new dependency; behaviour the request does not ask for; and any choice between options with real trade-offs.',\n\t\t'Do not ask what the repository, the request or a reasonable default already answers; find it out instead.',\n\t\t`Ask everything you need in one call: 1 to ${String(maxQuestions)} questions, each with ${String(minOptions)} to ${String(maxOptions)} options. Say in each option's description what choosing it means. The user may answer in their own words instead.`,\n\t].join(' '),\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tquestions: {\n\t\t\t\ttype: 'array',\n\t\t\t\tdescription: `1 to ${String(maxQuestions)} questions.`,\n\t\t\t\titems: {\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tquestion: { type: 'string', description: 'The whole question, ending with \"?\".' },\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\ttype: 'array',\n\t\t\t\t\t\t\tdescription: `${String(minOptions)} to ${String(maxOptions)} choices.`,\n\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\tlabel: { type: 'string', description: 'A few words.' },\n\t\t\t\t\t\t\t\t\tdescription: {\n\t\t\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t\t\t\tdescription: 'What choosing it means, and what it costs.',\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\trequired: ['label', 'description'],\n\t\t\t\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmultiSelect: {\n\t\t\t\t\t\t\ttype: 'boolean',\n\t\t\t\t\t\t\tdescription: 'Whether more than one option may be chosen.',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\trequired: ['question', 'options', 'multiSelect'],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\trequired: ['questions'],\n\t\tadditionalProperties: false,\n\t},\n};\n\n/** The questions in a call's input, or why they cannot be asked, which the model reads. */\nexport function readQuestions(input: unknown): UserQuestion[] | string {\n\tconst questions = isRecord(input) ? input.questions : undefined;\n\tif (!Array.isArray(questions) || questions.length === 0 || questions.length > maxQuestions) {\n\t\treturn `Ask 1 to ${String(maxQuestions)} questions.`;\n\t}\n\tconst read: UserQuestion[] = [];\n\tfor (const [index, question] of questions.entries()) {\n\t\tconst at = `Question ${String(index + 1)}`;\n\t\tif (!isRecord(question) || !isText(question.question)) return `${at} has no text.`;\n\t\tconst { options, multiSelect } = question;\n\t\tif (!Array.isArray(options) || options.length < minOptions || options.length > maxOptions) {\n\t\t\treturn `${at} needs ${String(minOptions)} to ${String(maxOptions)} options.`;\n\t\t}\n\t\tif (typeof multiSelect !== 'boolean') return `${at} must say whether it is multiSelect.`;\n\t\tconst choices = [];\n\t\tfor (const option of options) {\n\t\t\tif (!isRecord(option) || !isText(option.label) || typeof option.description !== 'string') {\n\t\t\t\treturn `Every option of ${at.toLowerCase()} needs a label and a description.`;\n\t\t\t}\n\t\t\tchoices.push({ label: option.label, description: option.description });\n\t\t}\n\t\tread.push({ question: question.question, options: choices, multiSelect });\n\t}\n\treturn read;\n}\n\n/**\n * The asker's answers, one non-empty string per question, or nothing: an asker is host code, so\n * an answer of any other shape is its bug, not the model's.\n */\nexport function readAnswers(answers: unknown, questions: UserQuestion[]): string[] | undefined {\n\tif (!Array.isArray(answers) || answers.length !== questions.length) return undefined;\n\treturn answers.every(isText) ? answers : undefined;\n}\n\n/** What the model reads back: each question with its answer. */\nexport function describeAnswers(questions: UserQuestion[], answers: string[]): string {\n\treturn [\n\t\t'The user answered:',\n\t\t...questions.map(\n\t\t\t({ question }, index) => `${String(index + 1)}. ${question}\\n ${answers[index] ?? ''}`,\n\t\t),\n\t].join('\\n');\n}\n\nexport function summarizeQuestions(questions: UserQuestion[]): string {\n\treturn questions.length === 1 ? '1 question' : `${String(questions.length)} questions`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isText(value: unknown): value is string {\n\treturn typeof value === 'string' && value.trim() !== '';\n}\n"]}
1
+ {"version":3,"file":"askUser.js","sourceRoot":"","sources":["../../../../src/engines/domain/model/askUser.ts"],"names":[],"mappings":"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","sourcesContent":["import type { UserQuestion } from '#src/agent/domain/question';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\n\nexport const askUserName = 'askUser';\n\nconst maxQuestions = 4;\nconst minOptions = 2;\nconst maxOptions = 4;\n\n// Strict mode allows no length bounds, so the counts live in the description and in `readQuestions`.\nexport const askUserDefinition: ToolDefinition = {\n\tname: askUserName,\n\tdescription: [\n\t\t'Asks the user to decide something that is theirs to decide, and waits for the answer.',\n\t\t'Ask before you build on a choice, not after. The user decides: the shape of a public API or of anything other code depends on; a new dependency; behaviour the request does not ask for; and any choice between options with real trade-offs.',\n\t\t'Do not ask what the repository, the request or a reasonable default already answers; find it out instead.',\n\t\t`Ask everything you need in one call: 1 to ${String(maxQuestions)} questions, each with ${String(minOptions)} to ${String(maxOptions)} options. Say in each option's description what choosing it means. The user may answer in their own words instead.`,\n\t].join(' '),\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tquestions: {\n\t\t\t\ttype: 'array',\n\t\t\t\tdescription: `1 to ${String(maxQuestions)} questions.`,\n\t\t\t\titems: {\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tquestion: { type: 'string', description: 'The whole question, ending with \"?\".' },\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\ttype: 'array',\n\t\t\t\t\t\t\tdescription: `${String(minOptions)} to ${String(maxOptions)} choices.`,\n\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\tlabel: { type: 'string', description: 'A few words.' },\n\t\t\t\t\t\t\t\t\tdescription: {\n\t\t\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t\t\t\tdescription: 'What choosing it means, and what it costs.',\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\trequired: ['label', 'description'],\n\t\t\t\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmultiSelect: {\n\t\t\t\t\t\t\ttype: 'boolean',\n\t\t\t\t\t\t\tdescription: 'Whether more than one option may be chosen.',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\trequired: ['question', 'options', 'multiSelect'],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\trequired: ['questions'],\n\t\tadditionalProperties: false,\n\t},\n};\n\n/** The questions in a call's input, or why they cannot be asked, which the model reads. */\nexport function readQuestions(input: unknown): UserQuestion[] | string {\n\tconst questions = isRecord(input) ? input.questions : undefined;\n\tif (!Array.isArray(questions) || questions.length === 0 || questions.length > maxQuestions) {\n\t\treturn `Ask 1 to ${String(maxQuestions)} questions.`;\n\t}\n\tconst read: UserQuestion[] = [];\n\tfor (const [index, question] of questions.entries()) {\n\t\tconst at = `Question ${String(index + 1)}`;\n\t\tif (!isRecord(question) || !isText(question.question)) return `${at} has no text.`;\n\t\tconst { options, multiSelect } = question;\n\t\tif (!Array.isArray(options) || options.length < minOptions || options.length > maxOptions) {\n\t\t\treturn `${at} needs ${String(minOptions)} to ${String(maxOptions)} options.`;\n\t\t}\n\t\tif (typeof multiSelect !== 'boolean') return `${at} must say whether it is multiSelect.`;\n\t\tconst choices = [];\n\t\tfor (const option of options) {\n\t\t\tif (!isRecord(option) || !isText(option.label) || typeof option.description !== 'string') {\n\t\t\t\treturn `Every option of ${at.toLowerCase()} needs a label and a description.`;\n\t\t\t}\n\t\t\tchoices.push({ label: option.label, description: option.description });\n\t\t}\n\t\tread.push({ question: question.question, options: choices, multiSelect });\n\t}\n\treturn read;\n}\n\n/** What the model reads back: each question with its answer. */\nexport function describeAnswers(questions: UserQuestion[], answers: string[]): string {\n\treturn [\n\t\t'The user answered:',\n\t\t...questions.map(\n\t\t\t({ question }, index) => `${String(index + 1)}. ${question}\\n ${answers[index] ?? ''}`,\n\t\t),\n\t].join('\\n');\n}\n\nexport function summarizeQuestions(questions: UserQuestion[]): string {\n\treturn questions.length === 1 ? '1 question' : `${String(questions.length)} questions`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isText(value: unknown): value is string {\n\treturn typeof value === 'string' && value.trim() !== '';\n}\n"]}
@@ -29,7 +29,7 @@ export declare function followHistory(conversation: Conversation, history: Histo
29
29
  /**
30
30
  * What the model is told when the workspace was moved back past some of its turns: that they
31
31
  * were undone and no longer show, what they asked, or a summary of what they tried, the files
32
- * those runs had changed, the files a move left as they were, and the user's reasons.
32
+ * those runs had changed, the files a move left as they were and why, and the user's reasons.
33
33
  */
34
34
  export declare function undoNote({ conversation, history, followed, summary, }: {
35
35
  conversation: Conversation;
@@ -54,7 +54,7 @@ export async function followHistory(conversation, history, seenRevision) {
54
54
  /**
55
55
  * What the model is told when the workspace was moved back past some of its turns: that they
56
56
  * were undone and no longer show, what they asked, or a summary of what they tried, the files
57
- * those runs had changed, the files a move left as they were, and the user's reasons.
57
+ * those runs had changed, the files a move left as they were and why, and the user's reasons.
58
58
  */
59
59
  export async function undoNote({ conversation, history, followed, summary, }) {
60
60
  const lines = [
@@ -75,6 +75,8 @@ export async function undoNote({ conversation, history, followed, summary, }) {
75
75
  }
76
76
  let root;
77
77
  const files = new Set();
78
+ // What a move leaves for this reason too, though nobody changed it since.
79
+ const uncopied = new Set();
78
80
  const runs = new Set(conversation
79
81
  .describeTurns()
80
82
  .filter(turn => followed.undone.includes(turn.index))
@@ -86,6 +88,10 @@ export async function undoNote({ conversation, history, followed, summary, }) {
86
88
  root = record.record.root;
87
89
  for (const entry of record.entries)
88
90
  files.add(entry.path);
91
+ for (const change of record.unrecorded) {
92
+ files.add(change.path);
93
+ uncopied.add(change.path);
94
+ }
89
95
  }
90
96
  if (files.size > 0) {
91
97
  lines.push('', `Files those runs had changed: ${[...files].map(path => relativeTo(root, path)).join(', ')}`);
@@ -93,8 +99,13 @@ export async function undoNote({ conversation, history, followed, summary, }) {
93
99
  const conflicts = [
94
100
  ...new Set(followed.revisions.flatMap(revision => revision.conflicts.map(conflict => conflict.path))),
95
101
  ];
96
- if (conflicts.length > 0) {
97
- lines.push('', `Files left as they were, because they had changed since: ${conflicts.map(path => relativeTo(root, path)).join(', ')}`);
102
+ const changedSince = conflicts.filter(path => !uncopied.has(path));
103
+ const notCopied = conflicts.filter(path => uncopied.has(path));
104
+ if (changedSince.length > 0) {
105
+ lines.push('', `Files left as they were, because they had changed since: ${changedSince.map(path => relativeTo(root, path)).join(', ')}`);
106
+ }
107
+ if (notCopied.length > 0) {
108
+ lines.push('', `Files left as they were, because the history cannot copy them (a folder, a link, or a file over 64 MB): ${notCopied.map(path => relativeTo(root, path)).join(', ')}`);
98
109
  }
99
110
  const reasons = followed.revisions.flatMap(revision => revision.reason ? [revision.reason] : []);
100
111
  if (reasons.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"historyFollower.js","sourceRoot":"","sources":["../../../../src/engines/domain/model/historyFollower.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Conversation } from '#src/llm/domain/conversation';\nimport type { Message } from '#src/llm/domain/message';\nimport type { RecoveryStore, Revision } from '#src/recovery/domain/recoveryStore';\nimport { HistoryExpiredError } from '#src/recovery/domain/runTree';\n\n/** What an agent reads of its workspace's history. */\nexport type HistoryReader = Pick<RecoveryStore, 'listRuns' | 'listRevisions' | 'readRun'>;\n\n/** Where the conversation goes on after the workspace moved, and what the moves left behind. */\nexport interface Followed {\n\t/** The turn the next one follows. */\n\tturn: number | undefined;\n\t/**\n\t * The turns on the path to the last turn recorded whose run the workspace no longer holds:\n\t * what the moves since undid.\n\t */\n\tundone: number[];\n\t/** The moves since the conversation last looked, oldest first. */\n\trevisions: Revision[];\n\t/** The latest move, so the next look starts after it. */\n\tlatestRevision: number;\n\t/** Where the workspace is: `undefined` for the start of its history. */\n\thead: string | undefined;\n}\n\n// A prompt is named, not repeated: enough to recognise it.\nconst promptChars = 300;\n\n/**\n * Picks the turn a conversation goes on from, given where the workspace is now. A turn belongs\n * to the run it wrote in; a turn that wrote nothing belongs to the run of the turn it followed.\n * Of the turns whose run is on the line from the start to the head, it picks the one nearest\n * the head, and of those the newest. A run retention chained into a later one counts as that\n * one, and a run retention removed is off every line.\n */\nexport async function followHistory(\n\tconversation: Conversation,\n\thistory: HistoryReader,\n\tseenRevision: number,\n): Promise<Followed> {\n\tconst { head, runs } = await history.listRuns();\n\tconst revisions = await history.listRevisions();\n\n\tconst keeper = new Map<string, string>();\n\tfor (const run of runs) {\n\t\tkeeper.set(run.runId, run.runId);\n\t\tfor (const absorbed of run.absorbed ?? []) keeper.set(absorbed, run.runId);\n\t}\n\tconst parents = new Map(runs.map(run => [run.runId, run.parentRunId]));\n\tconst line: string[] = [];\n\tfor (let runId = head; runId !== undefined; runId = parents.get(runId)) line.unshift(runId);\n\n\t// -1 is the start of the history; `undefined` is a turn off the line.\n\tconst depths: (number | undefined)[] = [];\n\tconst runsOf: (string | undefined)[] = [];\n\tlet best: number | undefined;\n\tfor (const { index, parent, runId } of conversation.describeTurns()) {\n\t\tconst run = runId ?? (parent === undefined ? undefined : runsOf[parent]);\n\t\trunsOf.push(run);\n\t\tconst kept = run === undefined ? undefined : keeper.get(run);\n\t\tconst position = kept === undefined ? -1 : line.indexOf(kept);\n\t\t// -1 for the start, -2 for a run off the line or no longer kept.\n\t\tconst own = run === undefined ? -1 : position === -1 ? -2 : position;\n\t\tconst onLine = own >= -1 && (parent === undefined || depths[parent] !== undefined);\n\t\tdepths.push(onLine ? own : undefined);\n\t\tconst bestDepth = best === undefined ? -2 : (depths[best] ?? -2);\n\t\tif (onLine && own >= bestDepth) best = index;\n\t}\n\n\tconst kept = new Set(conversation.path(best));\n\treturn {\n\t\tturn: best,\n\t\t// A turn left on another branch whose run is still on the line was not undone: a redo\n\t\t// passed it by. Only a turn whose run left the line was.\n\t\tundone: conversation\n\t\t\t.path(conversation.currentTurn)\n\t\t\t.filter(index => !kept.has(index) && depths[index] === undefined),\n\t\trevisions: revisions.filter(revision => revision.revision > seenRevision),\n\t\tlatestRevision: revisions.at(-1)?.revision ?? seenRevision,\n\t\thead,\n\t};\n}\n\n/**\n * What the model is told when the workspace was moved back past some of its turns: that they\n * were undone and no longer show, what they asked, or a summary of what they tried, the files\n * those runs had changed, the files a move left as they were, and the user's reasons.\n */\nexport async function undoNote({\n\tconversation,\n\thistory,\n\tfollowed,\n\tsummary,\n}: {\n\tconversation: Conversation;\n\thistory: HistoryReader;\n\tfollowed: Followed;\n\tsummary?: string;\n}): Promise<string> {\n\tconst lines = [\n\t\tfollowed.head === undefined\n\t\t\t? 'Note from the harness: the user moved the workspace back to how it was before any change an agent made. What the turns below asked for was undone, and those turns are no longer in this conversation.'\n\t\t\t: 'Note from the harness: the user moved the workspace back to an earlier run. What the turns below asked for was undone, and those turns are no longer in this conversation.',\n\t];\n\n\tif (summary) {\n\t\tlines.push('', 'What was tried, and what went wrong:', summary);\n\t} else {\n\t\tlines.push('', 'What they asked:');\n\t\tfor (const turn of followed.undone) {\n\t\t\tconst prompt = promptOf(conversation.messagesOf(turn)[0]);\n\t\t\tif (prompt !== undefined) lines.push(`- «${shorten(prompt, promptChars)}»`);\n\t\t}\n\t}\n\n\tlet root: string | undefined;\n\tconst files = new Set<string>();\n\tconst runs = new Set(\n\t\tconversation\n\t\t\t.describeTurns()\n\t\t\t.filter(turn => followed.undone.includes(turn.index))\n\t\t\t.flatMap(turn => (turn.runId ? [turn.runId] : [])),\n\t);\n\tfor (const runId of runs) {\n\t\tconst record = await changesOf(history, runId);\n\t\tif (!record) continue;\n\t\troot = record.record.root;\n\t\tfor (const entry of record.entries) files.add(entry.path);\n\t}\n\tif (files.size > 0) {\n\t\tlines.push(\n\t\t\t'',\n\t\t\t`Files those runs had changed: ${[...files].map(path => relativeTo(root, path)).join(', ')}`,\n\t\t);\n\t}\n\n\tconst conflicts = [\n\t\t...new Set(\n\t\t\tfollowed.revisions.flatMap(revision => revision.conflicts.map(conflict => conflict.path)),\n\t\t),\n\t];\n\tif (conflicts.length > 0) {\n\t\tlines.push(\n\t\t\t'',\n\t\t\t`Files left as they were, because they had changed since: ${conflicts.map(path => relativeTo(root, path)).join(', ')}`,\n\t\t);\n\t}\n\tconst reasons = followed.revisions.flatMap(revision =>\n\t\trevision.reason ? [revision.reason] : [],\n\t);\n\tif (reasons.length > 0) {\n\t\tlines.push('', `The user's reason: ${reasons.map(reason => `«${reason}»`).join(' ')}`);\n\t}\n\n\tlines.push(\n\t\t'',\n\t\t'Check the current state of a file before relying on what an earlier turn saw of it.',\n\t);\n\treturn lines.join('\\n');\n}\n\n/** The undone turns as plain text, for a model to summarize. Bounded, and marked when cut. */\nexport function transcriptOf(\n\tconversation: Conversation,\n\tturns: number[],\n\tmaxChars = 30_000,\n): string {\n\tconst lines: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const message of conversation.messagesOf(turn)) {\n\t\t\tfor (const part of message.content) {\n\t\t\t\tif (part.type === 'text') lines.push(`${message.role}: ${part.text}`);\n\t\t\t\tif (part.type === 'toolCall') {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t`${message.role} called ${part.name} ${shorten(JSON.stringify(part.input), 300)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (part.type === 'toolResult') {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t`${part.name} ${part.isError ? 'failed' : 'returned'}: ${shorten(part.output, 300)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst text = lines.join('\\n');\n\treturn text.length <= maxChars ? text : `${text.slice(0, maxChars)}\\n[the rest was cut]`;\n}\n\n/**\n * A run's record and changes. A run retention chained into a later one is read from the run that\n * holds its changes now, which names that run's own files too: they were undone with it, since a\n * run is only off the line when the run that holds it is. A run retention removed names nothing.\n */\nasync function changesOf(\n\thistory: HistoryReader,\n\trunId: string,\n): Promise<Awaited<ReturnType<HistoryReader['readRun']>> | undefined> {\n\tconst tried = new Set<string>();\n\tfor (let id: string | undefined = runId; id !== undefined && !tried.has(id);) {\n\t\ttried.add(id);\n\t\ttry {\n\t\t\treturn await history.readRun(id);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof HistoryExpiredError)) throw error;\n\t\t\tid = error.keptIn;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** The prompt of a turn: its first message's last text, after any note the harness put first. */\nfunction promptOf(message: Message | undefined): string | undefined {\n\tconst texts = message?.content.flatMap(part => (part.type === 'text' ? [part.text] : []));\n\treturn texts?.at(-1);\n}\n\nfunction shorten(text: string, chars: number): string {\n\tconst flat = text.replace(/\\s+/g, ' ').trim();\n\treturn flat.length <= chars ? flat : `${flat.slice(0, chars)}…`;\n}\n\nfunction relativeTo(root: string | undefined, path: string): string {\n\treturn root !== undefined && path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;\n}\n"]}
1
+ {"version":3,"file":"historyFollower.js","sourceRoot":"","sources":["../../../../src/engines/domain/model/historyFollower.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Conversation } from '#src/llm/domain/conversation';\nimport type { Message } from '#src/llm/domain/message';\nimport type { RecoveryStore, Revision } from '#src/recovery/domain/recoveryStore';\nimport { HistoryExpiredError } from '#src/recovery/domain/runTree';\n\n/** What an agent reads of its workspace's history. */\nexport type HistoryReader = Pick<RecoveryStore, 'listRuns' | 'listRevisions' | 'readRun'>;\n\n/** Where the conversation goes on after the workspace moved, and what the moves left behind. */\nexport interface Followed {\n\t/** The turn the next one follows. */\n\tturn: number | undefined;\n\t/**\n\t * The turns on the path to the last turn recorded whose run the workspace no longer holds:\n\t * what the moves since undid.\n\t */\n\tundone: number[];\n\t/** The moves since the conversation last looked, oldest first. */\n\trevisions: Revision[];\n\t/** The latest move, so the next look starts after it. */\n\tlatestRevision: number;\n\t/** Where the workspace is: `undefined` for the start of its history. */\n\thead: string | undefined;\n}\n\n// A prompt is named, not repeated: enough to recognise it.\nconst promptChars = 300;\n\n/**\n * Picks the turn a conversation goes on from, given where the workspace is now. A turn belongs\n * to the run it wrote in; a turn that wrote nothing belongs to the run of the turn it followed.\n * Of the turns whose run is on the line from the start to the head, it picks the one nearest\n * the head, and of those the newest. A run retention chained into a later one counts as that\n * one, and a run retention removed is off every line.\n */\nexport async function followHistory(\n\tconversation: Conversation,\n\thistory: HistoryReader,\n\tseenRevision: number,\n): Promise<Followed> {\n\tconst { head, runs } = await history.listRuns();\n\tconst revisions = await history.listRevisions();\n\n\tconst keeper = new Map<string, string>();\n\tfor (const run of runs) {\n\t\tkeeper.set(run.runId, run.runId);\n\t\tfor (const absorbed of run.absorbed ?? []) keeper.set(absorbed, run.runId);\n\t}\n\tconst parents = new Map(runs.map(run => [run.runId, run.parentRunId]));\n\tconst line: string[] = [];\n\tfor (let runId = head; runId !== undefined; runId = parents.get(runId)) line.unshift(runId);\n\n\t// -1 is the start of the history; `undefined` is a turn off the line.\n\tconst depths: (number | undefined)[] = [];\n\tconst runsOf: (string | undefined)[] = [];\n\tlet best: number | undefined;\n\tfor (const { index, parent, runId } of conversation.describeTurns()) {\n\t\tconst run = runId ?? (parent === undefined ? undefined : runsOf[parent]);\n\t\trunsOf.push(run);\n\t\tconst kept = run === undefined ? undefined : keeper.get(run);\n\t\tconst position = kept === undefined ? -1 : line.indexOf(kept);\n\t\t// -1 for the start, -2 for a run off the line or no longer kept.\n\t\tconst own = run === undefined ? -1 : position === -1 ? -2 : position;\n\t\tconst onLine = own >= -1 && (parent === undefined || depths[parent] !== undefined);\n\t\tdepths.push(onLine ? own : undefined);\n\t\tconst bestDepth = best === undefined ? -2 : (depths[best] ?? -2);\n\t\tif (onLine && own >= bestDepth) best = index;\n\t}\n\n\tconst kept = new Set(conversation.path(best));\n\treturn {\n\t\tturn: best,\n\t\t// A turn left on another branch whose run is still on the line was not undone: a redo\n\t\t// passed it by. Only a turn whose run left the line was.\n\t\tundone: conversation\n\t\t\t.path(conversation.currentTurn)\n\t\t\t.filter(index => !kept.has(index) && depths[index] === undefined),\n\t\trevisions: revisions.filter(revision => revision.revision > seenRevision),\n\t\tlatestRevision: revisions.at(-1)?.revision ?? seenRevision,\n\t\thead,\n\t};\n}\n\n/**\n * What the model is told when the workspace was moved back past some of its turns: that they\n * were undone and no longer show, what they asked, or a summary of what they tried, the files\n * those runs had changed, the files a move left as they were and why, and the user's reasons.\n */\nexport async function undoNote({\n\tconversation,\n\thistory,\n\tfollowed,\n\tsummary,\n}: {\n\tconversation: Conversation;\n\thistory: HistoryReader;\n\tfollowed: Followed;\n\tsummary?: string;\n}): Promise<string> {\n\tconst lines = [\n\t\tfollowed.head === undefined\n\t\t\t? 'Note from the harness: the user moved the workspace back to how it was before any change an agent made. What the turns below asked for was undone, and those turns are no longer in this conversation.'\n\t\t\t: 'Note from the harness: the user moved the workspace back to an earlier run. What the turns below asked for was undone, and those turns are no longer in this conversation.',\n\t];\n\n\tif (summary) {\n\t\tlines.push('', 'What was tried, and what went wrong:', summary);\n\t} else {\n\t\tlines.push('', 'What they asked:');\n\t\tfor (const turn of followed.undone) {\n\t\t\tconst prompt = promptOf(conversation.messagesOf(turn)[0]);\n\t\t\tif (prompt !== undefined) lines.push(`- «${shorten(prompt, promptChars)}»`);\n\t\t}\n\t}\n\n\tlet root: string | undefined;\n\tconst files = new Set<string>();\n\t// What a move leaves for this reason too, though nobody changed it since.\n\tconst uncopied = new Set<string>();\n\tconst runs = new Set(\n\t\tconversation\n\t\t\t.describeTurns()\n\t\t\t.filter(turn => followed.undone.includes(turn.index))\n\t\t\t.flatMap(turn => (turn.runId ? [turn.runId] : [])),\n\t);\n\tfor (const runId of runs) {\n\t\tconst record = await changesOf(history, runId);\n\t\tif (!record) continue;\n\t\troot = record.record.root;\n\t\tfor (const entry of record.entries) files.add(entry.path);\n\t\tfor (const change of record.unrecorded) {\n\t\t\tfiles.add(change.path);\n\t\t\tuncopied.add(change.path);\n\t\t}\n\t}\n\tif (files.size > 0) {\n\t\tlines.push(\n\t\t\t'',\n\t\t\t`Files those runs had changed: ${[...files].map(path => relativeTo(root, path)).join(', ')}`,\n\t\t);\n\t}\n\n\tconst conflicts = [\n\t\t...new Set(\n\t\t\tfollowed.revisions.flatMap(revision => revision.conflicts.map(conflict => conflict.path)),\n\t\t),\n\t];\n\tconst changedSince = conflicts.filter(path => !uncopied.has(path));\n\tconst notCopied = conflicts.filter(path => uncopied.has(path));\n\tif (changedSince.length > 0) {\n\t\tlines.push(\n\t\t\t'',\n\t\t\t`Files left as they were, because they had changed since: ${changedSince.map(path => relativeTo(root, path)).join(', ')}`,\n\t\t);\n\t}\n\tif (notCopied.length > 0) {\n\t\tlines.push(\n\t\t\t'',\n\t\t\t`Files left as they were, because the history cannot copy them (a folder, a link, or a file over 64 MB): ${notCopied.map(path => relativeTo(root, path)).join(', ')}`,\n\t\t);\n\t}\n\tconst reasons = followed.revisions.flatMap(revision =>\n\t\trevision.reason ? [revision.reason] : [],\n\t);\n\tif (reasons.length > 0) {\n\t\tlines.push('', `The user's reason: ${reasons.map(reason => `«${reason}»`).join(' ')}`);\n\t}\n\n\tlines.push(\n\t\t'',\n\t\t'Check the current state of a file before relying on what an earlier turn saw of it.',\n\t);\n\treturn lines.join('\\n');\n}\n\n/** The undone turns as plain text, for a model to summarize. Bounded, and marked when cut. */\nexport function transcriptOf(\n\tconversation: Conversation,\n\tturns: number[],\n\tmaxChars = 30_000,\n): string {\n\tconst lines: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const message of conversation.messagesOf(turn)) {\n\t\t\tfor (const part of message.content) {\n\t\t\t\tif (part.type === 'text') lines.push(`${message.role}: ${part.text}`);\n\t\t\t\tif (part.type === 'toolCall') {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t`${message.role} called ${part.name} ${shorten(JSON.stringify(part.input), 300)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (part.type === 'toolResult') {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t`${part.name} ${part.isError ? 'failed' : 'returned'}: ${shorten(part.output, 300)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst text = lines.join('\\n');\n\treturn text.length <= maxChars ? text : `${text.slice(0, maxChars)}\\n[the rest was cut]`;\n}\n\n/**\n * A run's record and changes. A run retention chained into a later one is read from the run that\n * holds its changes now, which names that run's own files too: they were undone with it, since a\n * run is only off the line when the run that holds it is. A run retention removed names nothing.\n */\nasync function changesOf(\n\thistory: HistoryReader,\n\trunId: string,\n): Promise<Awaited<ReturnType<HistoryReader['readRun']>> | undefined> {\n\tconst tried = new Set<string>();\n\tfor (let id: string | undefined = runId; id !== undefined && !tried.has(id);) {\n\t\ttried.add(id);\n\t\ttry {\n\t\t\treturn await history.readRun(id);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof HistoryExpiredError)) throw error;\n\t\t\tid = error.keptIn;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** The prompt of a turn: its first message's last text, after any note the harness put first. */\nfunction promptOf(message: Message | undefined): string | undefined {\n\tconst texts = message?.content.flatMap(part => (part.type === 'text' ? [part.text] : []));\n\treturn texts?.at(-1);\n}\n\nfunction shorten(text: string, chars: number): string {\n\tconst flat = text.replace(/\\s+/g, ' ').trim();\n\treturn flat.length <= chars ? flat : `${flat.slice(0, chars)}…`;\n}\n\nfunction relativeTo(root: string | undefined, path: string): string {\n\treturn root !== undefined && path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;\n}\n"]}