@mikode13/harness 11.0.1 → 11.2.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.
- package/README.md +186 -16
- package/dist/agent/domain/agent.d.ts +41 -3
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +20 -0
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/inbox.d.ts +42 -0
- package/dist/agent/domain/inbox.js +81 -0
- package/dist/agent/domain/inbox.js.map +1 -0
- package/dist/agent/domain/oneRunAtATime.d.ts +17 -0
- package/dist/agent/domain/oneRunAtATime.js +32 -0
- package/dist/agent/domain/oneRunAtATime.js.map +1 -0
- package/dist/agent/domain/runContext.d.ts +9 -0
- package/dist/agent/domain/runContext.js +5 -0
- package/dist/agent/domain/runContext.js.map +1 -1
- package/dist/agent/domain/session.d.ts +15 -0
- package/dist/agent/domain/session.js +80 -0
- package/dist/agent/domain/session.js.map +1 -0
- package/dist/engines/claude/infrastructure/model/claudeAgent.d.ts +6 -2
- package/dist/engines/claude/infrastructure/model/claudeAgent.js +20 -5
- package/dist/engines/claude/infrastructure/model/claudeAgent.js.map +1 -1
- package/dist/engines/codex/infrastructure/model/codexAgent.d.ts +5 -1
- package/dist/engines/codex/infrastructure/model/codexAgent.js +13 -2
- package/dist/engines/codex/infrastructure/model/codexAgent.js.map +1 -1
- package/dist/engines/domain/model/backgroundAgents.d.ts +5 -2
- package/dist/engines/domain/model/backgroundAgents.js +11 -3
- package/dist/engines/domain/model/backgroundAgents.js.map +1 -1
- package/dist/engines/domain/model/compactor.d.ts +40 -0
- package/dist/engines/domain/model/compactor.js +5 -0
- package/dist/engines/domain/model/compactor.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +53 -2
- package/dist/engines/domain/model/llmAgent.js +419 -24
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/engines/domain/model/recentCompaction.d.ts +39 -0
- package/dist/engines/domain/model/recentCompaction.js +119 -0
- package/dist/engines/domain/model/recentCompaction.js.map +1 -0
- package/dist/engines/domain/model/stepContext.js +3 -0
- package/dist/engines/domain/model/stepContext.js.map +1 -1
- package/dist/engines/domain/model/triagedCompaction.d.ts +27 -0
- package/dist/engines/domain/model/triagedCompaction.js +60 -0
- package/dist/engines/domain/model/triagedCompaction.js.map +1 -0
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +56 -15
- package/dist/factory/infrastructure/agentLLMFactory.js +144 -25
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/domain/contextTriage.d.ts +26 -0
- package/dist/llm/domain/contextTriage.js +3 -0
- package/dist/llm/domain/contextTriage.js.map +1 -0
- package/dist/llm/domain/llm.d.ts +18 -1
- package/dist/llm/domain/llm.js.map +1 -1
- package/dist/llm/domain/message.d.ts +18 -1
- package/dist/llm/domain/message.js +20 -0
- package/dist/llm/domain/message.js.map +1 -1
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +4 -5
- package/dist/llm/infrastructure/claudeLLMClient.js +39 -13
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/llm/infrastructure/jevContextTriage.d.ts +18 -0
- package/dist/llm/infrastructure/jevContextTriage.js +92 -0
- package/dist/llm/infrastructure/jevContextTriage.js.map +1 -0
- package/dist/llm/infrastructure/openAILLMClient.d.ts +4 -5
- package/dist/llm/infrastructure/openAILLMClient.js +20 -9
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +9 -1
- package/dist/orchestration/domain/model/orchestratorAgent.js +20 -3
- package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
- package/dist/recovery/domain/history.d.ts +10 -3
- package/dist/recovery/domain/history.js +1 -0
- package/dist/recovery/domain/history.js.map +1 -1
- package/dist/recovery/domain/recoveryStore.d.ts +14 -2
- package/dist/recovery/domain/recoveryStore.js.map +1 -1
- package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +21 -2
- package/dist/recovery/infrastructure/fileRecoveryStore.js +58 -6
- package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
- package/dist/recovery/infrastructure/folderClaims.d.ts +27 -0
- package/dist/recovery/infrastructure/folderClaims.js +124 -0
- package/dist/recovery/infrastructure/folderClaims.js.map +1 -0
- package/dist/shared/domain/session.d.ts +12 -0
- package/dist/shared/domain/session.js +2 -0
- package/dist/shared/domain/session.js.map +1 -0
- package/dist/tools/domain/preparedCall.d.ts +5 -2
- package/dist/tools/domain/preparedCall.js +7 -1
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/domain/readRegistry.d.ts +11 -0
- package/dist/tools/domain/readRegistry.js.map +1 -1
- package/dist/tools/infrastructure/agentAsTool.d.ts +6 -0
- package/dist/tools/infrastructure/agentAsTool.js +9 -0
- package/dist/tools/infrastructure/agentAsTool.js.map +1 -1
- package/dist/tools/infrastructure/commandTool.js +6 -1
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.js +2 -0
- package/dist/tools/infrastructure/exploreTool.js.map +1 -1
- package/dist/tools/infrastructure/fileTools.d.ts +2 -0
- package/dist/tools/infrastructure/fileTools.js +10 -1
- package/dist/tools/infrastructure/fileTools.js.map +1 -1
- package/dist/tools/infrastructure/removeWorktreeTool.d.ts +23 -0
- package/dist/tools/infrastructure/removeWorktreeTool.js +178 -0
- package/dist/tools/infrastructure/removeWorktreeTool.js.map +1 -0
- package/dist/tools/infrastructure/subagents.d.ts +3 -1
- package/dist/tools/infrastructure/subagents.js +2 -1
- package/dist/tools/infrastructure/subagents.js.map +1 -1
- package/dist/tools/infrastructure/trackedEdits.js +1 -0
- package/dist/tools/infrastructure/trackedEdits.js.map +1 -1
- package/dist/tools/infrastructure/workspaceWrites.d.ts +17 -1
- package/dist/tools/infrastructure/workspaceWrites.js +48 -17
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
- package/dist/tools/infrastructure/worktrees.d.ts +49 -0
- package/dist/tools/infrastructure/worktrees.js +154 -0
- package/dist/tools/infrastructure/worktrees.js.map +1 -0
- package/dist/tools/infrastructure/writeCeiling.d.ts +16 -0
- package/dist/tools/infrastructure/writeCeiling.js +49 -0
- package/dist/tools/infrastructure/writeCeiling.js.map +1 -0
- package/dist/tools/infrastructure/writeSession.d.ts +5 -1
- package/dist/tools/infrastructure/writeSession.js +7 -3
- package/dist/tools/infrastructure/writeSession.js.map +1 -1
- package/dist/workflow/domain/workflow.d.ts +3 -0
- package/dist/workflow/domain/workflow.js.map +1 -1
- package/dist/workflow/domain/workflowRunner.js +8 -2
- package/dist/workflow/domain/workflowRunner.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -446,6 +446,54 @@ it to correct. An asker that throws, or does not answer each question with some
|
|
|
446
446
|
ends the run with an `UnrecoverableError`. The Agent SDK engines ignore `ask`, and
|
|
447
447
|
`createLLMOrchestrator` passes it to every role.
|
|
448
448
|
|
|
449
|
+
### One run at a time, and what the user sends meanwhile
|
|
450
|
+
|
|
451
|
+
An agent continues one conversation, so it takes one run at a time: a `run()` that starts
|
|
452
|
+
while another run on the same agent works rejects with `RunInProgressError`, an
|
|
453
|
+
`UnrecoverableError`, and changes nothing. Queue what the user sends meanwhile, and run it
|
|
454
|
+
once the run in progress ends. For one chat, keep one agent; for another chat, create another.
|
|
455
|
+
|
|
456
|
+
A message the user wants read at once goes to the run in progress through an `Inbox`:
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { Inbox } from '@mikode13/harness';
|
|
460
|
+
|
|
461
|
+
const inbox = new Inbox();
|
|
462
|
+
const running = agent.run('Add the export button', { signal, inbox });
|
|
463
|
+
// Later, while it works:
|
|
464
|
+
inbox.send('Make it a dropdown too');
|
|
465
|
+
await running;
|
|
466
|
+
// What the run did not keep, such as a message sent after its final answer, is the next turn's.
|
|
467
|
+
const unread = inbox.close();
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
`createLLMAgent` reads what was sent once the step in progress ends, without cutting a model
|
|
471
|
+
call or a tool, as a message from the user. It also reads it while it waits for agents working
|
|
472
|
+
in the background, so the model can reconsider at once. The Agent SDK engines, the
|
|
473
|
+
orchestrators and the workflows never read it, so `close()` returns everything sent to them. Neither does a
|
|
474
|
+
run's last step, which has no step left to answer it. A run that fails before it is kept gives
|
|
475
|
+
back what it read: a retried attempt reads it again, or `close()` returns it.
|
|
476
|
+
|
|
477
|
+
### Sessions
|
|
478
|
+
|
|
479
|
+
Every run belongs to a session. Name it in `RunOptions.session`, such as your API's session
|
|
480
|
+
id with a name to show; without one, the run belongs to the agent's own session, which the
|
|
481
|
+
harness makes for each agent with an id a person can read, such as `smart-hawk-4821`.
|
|
482
|
+
`AgentResponse.session` returns the session the run belonged to, so a consumer that builds
|
|
483
|
+
its agent again passes it back to stay in the same session:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
const first = await agent.run('Add the export button', { signal });
|
|
487
|
+
// Later, maybe with an agent built again:
|
|
488
|
+
await other.run('Make it a dropdown too', { signal, session: first.session });
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
An agent another agent started belongs to the outer run's session, and a workflow makes a
|
|
492
|
+
new one for each run that names none. A session without a `name`, or with a blank one, is shown by its `id`. Each
|
|
493
|
+
run that writes records its session in the workspace's history, as `HistoryRun.session`, so
|
|
494
|
+
a host can say whose change an undo takes back. It is the harness's session, not a
|
|
495
|
+
provider's, which stays inside its engine.
|
|
496
|
+
|
|
449
497
|
### Agents that change files
|
|
450
498
|
|
|
451
499
|
`createFileTools` gives an agent the file tools of its provider, over the folders you
|
|
@@ -475,6 +523,11 @@ const agent = createLLMAgent('openai', {
|
|
|
475
523
|
});
|
|
476
524
|
```
|
|
477
525
|
|
|
526
|
+
`workspace` gives no tools: it tells the agent where it works and has it follow the
|
|
527
|
+
history. An agent given a workspace and no tools at all is refused with an
|
|
528
|
+
`InvalidAgentConfigError`; one with only tools of your own is built, and one with neither
|
|
529
|
+
works from its prompt alone.
|
|
530
|
+
|
|
478
531
|
Every tool goes through one access policy:
|
|
479
532
|
|
|
480
533
|
- it refuses a path outside the roots;
|
|
@@ -505,7 +558,8 @@ has no stable shape; read it, but do not build on it. A callback that fails is l
|
|
|
505
558
|
run stays as it ended.
|
|
506
559
|
|
|
507
560
|
`createLLMOrchestrator()` takes the options of `createOrchestrator()` plus `workspace`,
|
|
508
|
-
`maxSteps`, `compactAt`, `commands`, `formatter`, `summarizeUndone
|
|
561
|
+
`maxSteps`, `compactAt`, `commands`, `formatter`, `summarizeUndone`, `onTranscript`,
|
|
562
|
+
`removeWorktree` and `agents`, and returns a promise. Every role runs on the
|
|
509
563
|
model APIs:
|
|
510
564
|
|
|
511
565
|
- the planner and the reviewer read, with a `showChanges` tool that gives the current task's
|
|
@@ -519,7 +573,15 @@ model APIs:
|
|
|
519
573
|
`subagent` events;
|
|
520
574
|
- the executor gets its provider's file tools. `autoApprove` reaches only the executor;
|
|
521
575
|
- each role gets the commands whose `roles` name it; a command names only the executor unless
|
|
522
|
-
it says otherwise
|
|
576
|
+
it says otherwise;
|
|
577
|
+
- each role gets the agents `agents` gives it, from `agentAsTool`, beside its own tools: the
|
|
578
|
+
planner and the reviewer keep their explorers. They only read, so their agents only read too;
|
|
579
|
+
the executor's may write within the `write` roots.
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
// Each from agentAsTool, as in "Agents that hand work to other agents" below.
|
|
583
|
+
createLLMOrchestrator({ workspace, agents: { planner: [architect], executor: [commit] } });
|
|
584
|
+
```
|
|
523
585
|
|
|
524
586
|
Each role's run stops after a number of calls to the model: 80 for the executor and 65 for
|
|
525
587
|
the planner and the reviewer by default. `maxSteps` changes it, as one number for every role
|
|
@@ -541,18 +603,39 @@ once, **Stop** ends it, and words of your own reach the model. A run stopped thi
|
|
|
541
603
|
`ask` to ask, fails with `UnrecoverableError`, and is kept in the conversation with its changes,
|
|
542
604
|
so your next prompt continues from it.
|
|
543
605
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
606
|
+
Each agent's context is compacted once it passes its model's threshold: 450,000 tokens for
|
|
607
|
+
Claude Opus, Fable and Sonnet and GPT astra, sol and terra, and 125,000 for Claude Haiku and
|
|
608
|
+
GPT luna. The provider compacts it on its side, inside the call that passes the threshold,
|
|
609
|
+
replacing the turns before that call with its own summary, which only its model reads back; the
|
|
610
|
+
conversation keeps every original turn. The call that compacts can lose its cache, as it did on
|
|
611
|
+
OpenAI, so keep the threshold well above what a task needs. `compactAt` changes it: like
|
|
612
|
+
`maxSteps`, one number for every role or per role, and `createLLMAgent` takes one; on Anthropic
|
|
613
|
+
it must be at least 50,000. `false` turns compaction off:
|
|
551
614
|
|
|
552
615
|
```ts
|
|
553
616
|
createLLMOrchestrator({ workspace, compactAt: { executor: 200_000 } });
|
|
554
617
|
```
|
|
555
618
|
|
|
619
|
+
`compaction: 'recent'` has the harness compact it instead, for the agent and for every role:
|
|
620
|
+
before the next call, the agent's own model summarizes the context, reading it from its
|
|
621
|
+
provider's cache, and only the run's prompt and its last 10 answers, each with its tool
|
|
622
|
+
results, are kept word for word after the summary, without their signed reasoning; earlier runs
|
|
623
|
+
are always summarized. The
|
|
624
|
+
conversation still keeps every turn; only what the model is sent starts at the summary.
|
|
625
|
+
|
|
626
|
+
`compaction: 'jev'` summarizes the same way, but TypeSafe's Jev chooses what is kept: the run's
|
|
627
|
+
prompt, the model's last answer with its tool results, and each earlier tool result Jev judges
|
|
628
|
+
the agent still needs word for word, with its call. Jev is sent the run's prompt, the model's
|
|
629
|
+
last answer and each of those tool results, so what the tools read leaves for TypeSafe. It reads
|
|
630
|
+
its key from `TYPESAFE_API_KEY`;
|
|
631
|
+
without one, or when Jev fails, the recent choice is made instead, and logged.
|
|
632
|
+
|
|
633
|
+
Either way, a context that no longer fits the model is compacted once and the call tried again;
|
|
634
|
+
in the recent mode the model summarizes a transcript of it. When it still does not fit, the run
|
|
635
|
+
fails with `UnrecoverableError`. A `compaction` event says who compacted, why, and how many
|
|
636
|
+
tokens of context it compacted: after the `step` of the call that compacted, or once the
|
|
637
|
+
harness's summary is written. The summary's tokens count in the run's.
|
|
638
|
+
|
|
556
639
|
Without `workspace` it works on the current directory, read only. One run of the
|
|
557
640
|
orchestrator is one run of the history, every round included.
|
|
558
641
|
|
|
@@ -588,6 +671,13 @@ The agent works inside the caller's run:
|
|
|
588
671
|
|
|
589
672
|
- what it writes is the same run of the history, so one undo reverts both; it keeps its own
|
|
590
673
|
tools' access policy, and edits only what it read itself;
|
|
674
|
+
- it never writes more than the agent that called it: only within the `write` roots of that
|
|
675
|
+
agent's `workspace`, and nowhere when that workspace only reads. A calling agent with no
|
|
676
|
+
`workspace`, such as a coordinator that only hands out tasks, sets no limit of its own. Its
|
|
677
|
+
tools that may write elsewhere, a command included, are refused before anyone is asked, and
|
|
678
|
+
the model is told why. Where your own `Tool` writes is unknown, so under an agent that only
|
|
679
|
+
reads, a call your tool judges `mutating` or `destructive` is refused too; under an agent
|
|
680
|
+
that writes, it writes wherever your tool does;
|
|
591
681
|
- what it spends counts in the caller's `tokens`, a failed attempt included;
|
|
592
682
|
- it asks and is approved through the run's `ask` and `approve`, which agents working at once
|
|
593
683
|
may call before an earlier call has its answer; one person at one terminal needs them queued,
|
|
@@ -699,12 +789,13 @@ await history.redo();
|
|
|
699
789
|
await history.goTo(historyStart); // as it was before the agent's first run
|
|
700
790
|
```
|
|
701
791
|
|
|
702
|
-
`list()` gives every run kept, oldest first, with its parent, status
|
|
703
|
-
|
|
704
|
-
going back and writing again starts a new branch, and `goTo`
|
|
705
|
-
returns towards where the workspace was most recently. A
|
|
706
|
-
`AgentResponse` when it changed a file, and a run that failed or
|
|
707
|
-
writing is still listed, with that status. A run that changed nothing
|
|
792
|
+
`list()` gives every run kept, oldest first, with its parent, status, the files it changed
|
|
793
|
+
and the session it came from, and the run the workspace is at (`head`), absent at the
|
|
794
|
+
start. Runs form a tree: going back and writing again starts a new branch, and `goTo`
|
|
795
|
+
reaches the old one. `redo` returns towards where the workspace was most recently. A
|
|
796
|
+
run's `runId` is in its `AgentResponse` when it changed a file, and a run that failed or
|
|
797
|
+
was cancelled after writing is still listed, with that status. A run that changed nothing
|
|
798
|
+
is not kept.
|
|
708
799
|
|
|
709
800
|
A move writes a file only if it still holds what the history expects. A file you
|
|
710
801
|
changed since is left as it is and named in the move's `conflicts`, and the move is
|
|
@@ -736,6 +827,84 @@ The history is not version control. It holds only what the harness's own tools w
|
|
|
736
827
|
not your edits, not other programs', and not the Agent SDK engines' changes. The lock
|
|
737
828
|
keeps a second harness run out, but not an editor.
|
|
738
829
|
|
|
830
|
+
### Working apart in a worktree
|
|
831
|
+
|
|
832
|
+
Runs that only read never hold the workspace, so several chats can read and plan in one
|
|
833
|
+
folder at once. Only one run at a time writes there. For runs that write side by side,
|
|
834
|
+
give each its own git worktree: a folder of its own, with a history of its own.
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
import { createFileTools, createWorktree, type WorkspaceOptions } from '@mikode13/harness';
|
|
838
|
+
|
|
839
|
+
const { path } = await createWorktree({
|
|
840
|
+
repository: process.cwd(),
|
|
841
|
+
branch: 'feat/export-button',
|
|
842
|
+
from: 'main',
|
|
843
|
+
});
|
|
844
|
+
const workspace: WorkspaceOptions = { roots: [{ path, access: 'write' }] };
|
|
845
|
+
const agent = createLLMAgent('openai', {
|
|
846
|
+
systemPrompt: 'You build the export button, with a test for it.',
|
|
847
|
+
tools: await createFileTools('openai', workspace),
|
|
848
|
+
workspace,
|
|
849
|
+
});
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
The worktree checks out `branch`, a new branch started at `from`. Without `path` it goes
|
|
853
|
+
beside the repository's main folder, in `../<repository>-worktrees/<branch>`, with any
|
|
854
|
+
`/` in the branch made a `-`. It fails with an `UnrecoverableError` when the branch
|
|
855
|
+
exists or is not a valid name, `from` names no commit, or the folder exists and is not
|
|
856
|
+
empty. A failure leaves no branch or folder of its own behind.
|
|
857
|
+
|
|
858
|
+
Worktrees need git 2.36 or later, the first whose `worktree list` can name a folder whose
|
|
859
|
+
path holds a newline. With an older git, `createWorktree` fails with an
|
|
860
|
+
`UnrecoverableError`, and `removeWorktree: true` fails with an `InvalidAgentConfigError`
|
|
861
|
+
when the tools are created; both name the version found.
|
|
862
|
+
|
|
863
|
+
The harness never removes a worktree. An agent given `removeWorktree: true`, in
|
|
864
|
+
`createFileTools` or `createLLMOrchestrator` for its executor, can remove one when the
|
|
865
|
+
user asks. It removes the folder and every file in it, never the branch, which keeps
|
|
866
|
+
its commits. Each call asks the approver, and the request's `worktree` says what is
|
|
867
|
+
lost: `uncommitted` counts the files with changes no commit has, `ignored` the paths
|
|
868
|
+
`.gitignore` excludes, such as `.env` or `node_modules`, a folder once, and
|
|
869
|
+
`commitsOnlyHere` the commits only a detached HEAD holds. An ignored folder is never
|
|
870
|
+
looked into, so what is written in it while the user is asked goes too: when `ignored`
|
|
871
|
+
is not zero, say so in the question. The tool's answer tells the model the same. The
|
|
872
|
+
tool refuses the repository's main folder, a worktree the agent works in, and a locked
|
|
873
|
+
one. Once approved, it claims the worktree, so no harness run starts writing in it,
|
|
874
|
+
and removes nothing while a run may write there, from the worktree, a folder inside it
|
|
875
|
+
or a second root, or if the worktree changed while the user was asked: the model then
|
|
876
|
+
calls it again, and the user is asked anew.
|
|
877
|
+
|
|
878
|
+
### Rules for runs that share a folder
|
|
879
|
+
|
|
880
|
+
The harness coordinates its own runs on one folder by these rules. Each refusal says which
|
|
881
|
+
rule stopped it, worded so the model can tell the user why it cannot go on.
|
|
882
|
+
|
|
883
|
+
1. **An agent takes one run at a time.** A second `run()` on a busy agent rejects with
|
|
884
|
+
`RunInProgressError`. Queue the message, or send it to the run in progress through
|
|
885
|
+
its `Inbox`.
|
|
886
|
+
2. **Reading never waits.** Any number of runs read and plan in one folder at once.
|
|
887
|
+
3. **One run at a time writes to a workspace.** A run holds its workspace from its first
|
|
888
|
+
write until it ends, and so does an undo, a redo or a `goTo`. Meanwhile another run's
|
|
889
|
+
write is refused and the model is told why, and a move rejects with
|
|
890
|
+
`WorkspaceBusyError`. For runs that write side by side, give each its own worktree.
|
|
891
|
+
4. **No worktree is removed while a harness run may write in it,** from the worktree, from
|
|
892
|
+
a folder inside it, or through a second root, and no run starts writing in a worktree
|
|
893
|
+
being removed. An undo or redo protects every file it writes, wherever it is, even
|
|
894
|
+
when the history was opened by its first root alone.
|
|
895
|
+
5. **In one run, every set of tools that writes to one history may write only where the
|
|
896
|
+
first set may.** A run claims the folders of the tools that wrote first; tools built
|
|
897
|
+
apart, with another `write` root, are refused, and the model is told why. Give every
|
|
898
|
+
set the same `write` roots, or build the widest first.
|
|
899
|
+
6. **What is removed is what was approved.** If a worktree changed while the user was
|
|
900
|
+
asked, nothing is removed, and the user is asked again. The inside of a folder
|
|
901
|
+
`.gitignore` excludes whole is not watched (rule 7).
|
|
902
|
+
7. **The harness sees only its own runs** that keep their history in the same state
|
|
903
|
+
directory. An editor, a terminal, another program, an Agent SDK engine or a history
|
|
904
|
+
kept elsewhere is not held back by any of these rules, and the inside of an ignored
|
|
905
|
+
folder is never looked into. Writing by hand in a folder an agent is writing in can
|
|
906
|
+
overwrite either side; keeping them apart is up to whoever does it.
|
|
907
|
+
|
|
739
908
|
## Tests
|
|
740
909
|
|
|
741
910
|
`pnpm test` runs the unit suite against deterministic fakes; it never contacts a
|
|
@@ -792,7 +961,8 @@ is told to decide and say what it assumed.
|
|
|
792
961
|
With `--llm`, the agents can also [run commands](#running-the-projects-commands) you
|
|
793
962
|
configure for the repository, in `~/.config/mikode-harness/config.json` (or under
|
|
794
963
|
`$XDG_CONFIG_HOME`). The file maps each repository's real path to its `commands` and,
|
|
795
|
-
optionally, its `maxSteps`, `compactAt
|
|
964
|
+
optionally, its `maxSteps`, its `compactAt`, or `false` to turn compaction off, its
|
|
965
|
+
`compaction` (`provider`, `recent` or `jev`, as `createLLMOrchestrator` takes them) and [`formatter`](#formatting-what-the-agent-writes). It lives outside every repository, so neither a cloned repository
|
|
796
966
|
nor the agent decides what runs, or for how long:
|
|
797
967
|
|
|
798
968
|
```json
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Tokens } from '#src/shared/domain/tokens';
|
|
2
2
|
import type { Approver } from './approval.ts';
|
|
3
|
+
import type { Inbox } from './inbox.ts';
|
|
3
4
|
import type { Asker } from './question.ts';
|
|
5
|
+
import type { Session } from '#src/shared/domain/session';
|
|
4
6
|
/**
|
|
5
7
|
* How a run ended when it did not fail. Every token a run spends travels with its end: here
|
|
6
8
|
* when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.
|
|
@@ -21,13 +23,19 @@ export interface AgentResponse {
|
|
|
21
23
|
* the history does not record.
|
|
22
24
|
*/
|
|
23
25
|
runId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The session the run belonged to: the one `RunOptions.session` gave, or the agent's own,
|
|
28
|
+
* which the harness creates for each agent. Pass it to later runs, on this agent or one built
|
|
29
|
+
* again, to keep them in the same session. Every agent the harness builds sets it.
|
|
30
|
+
*/
|
|
31
|
+
session?: Session;
|
|
24
32
|
}
|
|
25
33
|
export type Callback = (item: ProgressEvent) => void;
|
|
26
34
|
/**
|
|
27
35
|
* What a run needs besides its prompt. InstructedAgent passes it on whole. OrchestratorAgent
|
|
28
|
-
* copies its own enumerable fields
|
|
29
|
-
* the same for a run with `ask`, replacing only `ask`. A field must therefore
|
|
30
|
-
* enumerable one to reach every agent.
|
|
36
|
+
* copies its own enumerable fields, replaces `onProgress` per role and drops `inbox`;
|
|
37
|
+
* RetryingAgent does the same for a run with `ask`, replacing only `ask`. A field must therefore
|
|
38
|
+
* be an own enumerable one to reach every agent.
|
|
31
39
|
*/
|
|
32
40
|
export interface RunOptions {
|
|
33
41
|
/**
|
|
@@ -51,6 +59,19 @@ export interface RunOptions {
|
|
|
51
59
|
* its answer.
|
|
52
60
|
*/
|
|
53
61
|
ask?: Asker | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Messages sent to the run while it works. A model-backed agent reads them once the step in
|
|
64
|
+
* progress ends, as a message from the user, and wakes for them while it waits for agents in
|
|
65
|
+
* the background. The Agent SDK engines, the orchestrators and the workflows ignore it, so
|
|
66
|
+
* `Inbox.close` returns everything sent to them.
|
|
67
|
+
*/
|
|
68
|
+
inbox?: Inbox | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The session this run belongs to, such as the API's, recorded with each run that writes.
|
|
71
|
+
* Without it, the run belongs to the agent's own session, the same for each of its runs, which
|
|
72
|
+
* `AgentResponse.session` returns. An agent another agent started belongs to the outer run's.
|
|
73
|
+
*/
|
|
74
|
+
session?: Session | undefined;
|
|
54
75
|
}
|
|
55
76
|
/** The `onProgress` of a run that gave none. */
|
|
56
77
|
export declare const ignoreProgress: Callback;
|
|
@@ -146,6 +167,23 @@ export type ProgressEvent = {
|
|
|
146
167
|
context: StepContext;
|
|
147
168
|
agent?: ProgressAgent;
|
|
148
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* The context an agent on the model APIs sends was compacted: summarized from some point back,
|
|
172
|
+
* while its conversation keeps every part. `by` is who compacted it, the model's provider or
|
|
173
|
+
* the harness. `reason` is `threshold` when the context passed the agent's `compactAt`, and
|
|
174
|
+
* `maxContext` when it no longer fit the model, which compacts it even with compaction off.
|
|
175
|
+
* `contextTokens` is how many tokens of context were compacted, when known. The provider's
|
|
176
|
+
* compaction follows the `step` event of the call that compacted, whose tokens include the
|
|
177
|
+
* compaction's. The harness's is narrated once its summary is written, before the next call's
|
|
178
|
+
* `step`: the summary call has no `step` of its own, and its tokens are only in the run's.
|
|
179
|
+
*/
|
|
180
|
+
| {
|
|
181
|
+
type: 'compaction';
|
|
182
|
+
by: 'provider' | 'harness';
|
|
183
|
+
reason: 'threshold' | 'maxContext';
|
|
184
|
+
contextTokens?: number;
|
|
185
|
+
agent?: ProgressAgent;
|
|
186
|
+
}
|
|
149
187
|
/**
|
|
150
188
|
* A workflow's stage asks its agent for an answer; `attempt` is 2 or more when the previous
|
|
151
189
|
* answer was invalid. Stages run one at a time, so the events that follow, until the next
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AA0DA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\nimport type { Asker } from './question.ts';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n\t/**\n\t * The run the workspace's history recorded, when this run wrote through the harness's write\n\t * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes\n\t * the history does not record.\n\t */\n\trunId?: string;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/**\n * What a run needs besides its prompt. InstructedAgent passes it on whole. OrchestratorAgent\n * copies its own enumerable fields and replaces only `onProgress` per role; RetryingAgent does\n * the same for a run with `ask`, replacing only `ask`. A field must therefore be an own\n * enumerable one to reach every agent.\n */\nexport interface RunOptions {\n\t/**\n\t * Cancels the run, which then rejects with the signal's reason: an `AbortError`, or a\n\t * `TimeoutError` from `AbortSignal.timeout`. `isAbortError` recognizes both.\n\t */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide. Agents a delegation runs at once may call it before\n\t * an earlier call has its answer.\n\t */\n\tapprove?: Approver | undefined;\n\t/**\n\t * Answers what a model-backed agent asks the user, through its `askUser` tool, when a\n\t * decision is the user's to make. Without it the agent has no such tool. The Agent SDK\n\t * engines ignore it. Agents a delegation runs at once may call it before an earlier call has\n\t * its answer.\n\t */\n\task?: Asker | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number; agent?: ProgressAgent }\n\t| { type: 'reasoning'; message: string; agent?: ProgressAgent }\n\t| { type: 'search'; query: string; agent?: ProgressAgent }\n\t| {\n\t\t\ttype: 'fileChange';\n\t\t\tchanges: { path: string; kind: 'add' | 'update' | 'delete' }[];\n\t\t\tagent?: ProgressAgent;\n\t }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string; agent?: ProgressAgent }\n\t| { type: 'agentMessage'; message: string; agent?: ProgressAgent }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tagent?: ProgressAgent;\n\t\t\t/** A short, tool-owned description of the call. Narration only. */\n\t\t\tsummary?: string;\n\t\t\t/** The exit code of a configured command that ended with an error. Narration only. */\n\t\t\texitCode?: number;\n\t\t\t/**\n\t\t\t * On `error`, for an agent on the model APIs: what the model was told went wrong, such\n\t\t\t * as a patch that did not apply or a failed command's output. Narration only.\n\t\t\t */\n\t\t\terror?: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it. `sent` when\n\t\t\t * it sent subagents to work in the background, such as an `explore` call: they go on\n\t\t\t * after it, as `subagent` events, until a `subagentEnded` for each.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied' | 'sent';\n\t\t\t/**\n\t\t\t * On `completed`, for a call that changed a file through the harness's write tools: the\n\t\t\t * change as a unified diff, for the person watching. Narration only; the run's whole\n\t\t\t * record is the history's `changes`.\n\t\t\t */\n\t\t\tdiff?: string;\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[]; agent?: ProgressAgent }\n\t| { type: 'turnStarted'; agent?: ProgressAgent }\n\t| { type: 'turnEnded'; agent?: ProgressAgent }\n\t/**\n\t * One billed call to the model by an agent on the model APIs, including one the model\n\t * stopped early or the client turned into an error: what the call sent and what it cost.\n\t * Narration only: the run's total is `AgentResponse.tokens`, or the failure's `tokens`.\n\t */\n\t| {\n\t\t\ttype: 'step';\n\t\t\t/** 1 for the first call of the run. */\n\t\t\tstep: number;\n\t\t\t/** Missing when the provider did not report this call's usage. */\n\t\t\ttokens?: Tokens;\n\t\t\tcontext: StepContext;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A workflow's stage asks its agent for an answer; `attempt` is 2 or more when the previous\n\t * answer was invalid. Stages run one at a time, so the events that follow, until the next\n\t * workflow event, are this stage's agent's.\n\t */\n\t| { type: 'workflowStage'; stage: string; attempt: number; agent?: ProgressAgent }\n\t/** The transition a stage's valid answer selected, and the stage it leads to. */\n\t| {\n\t\t\ttype: 'workflowTransition';\n\t\t\tstage: string;\n\t\t\ttransition: 'go' | 'stop' | 'complete';\n\t\t\t/** Absent after `stop` or `complete`. */\n\t\t\tnextStage?: string;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * An event of a subagent that a tool call started: one of the explorers an `explore` call\n\t * sends, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that\n\t * call's `tool` events, and `subagent` numbers its subagents from 1, so the events of agents\n\t * running in parallel stay apart. Subagents work in the background: the call's own `tool`\n\t * event ends with `status: 'sent'`, and each subagent's events keep arriving after it, until\n\t * its `subagentEnded`, whether it answered, failed or was stopped. A subagent's own subagents\n\t * arrive as `subagent` events inside this one's `event`.\n\t */\n\t| {\n\t\t\ttype: 'subagent';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\t/** Which agent it is: `explorer`, or the name of the tool that delegates to it. */\n\t\t\tname: string;\n\t\t\t/** What the subagent was asked. */\n\t\t\ttask: string;\n\t\t\tevent: ProgressEvent;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A subagent that a call with `status: 'sent'` sent has ended: it `answered`, it `failed` and\n\t * its answer says why, or it was `stopped` because the run ended. `callId`, `subagent` and\n\t * `name` are those of its `subagent` events. Narration only: its answer reaches the model that\n\t * sent it, not the consumer.\n\t */\n\t| {\n\t\t\ttype: 'subagentEnded';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\tname: string;\n\t\t\toutcome: 'answered' | 'failed' | 'stopped';\n\t\t\tagent?: ProgressAgent;\n\t };\n\n/**\n * What one call to the model sent, in characters by where it came from: an estimate of size\n * for finding what fills a context, not a token count.\n */\nexport interface StepContext {\n\t/** The system prompt. */\n\tinstructions: number;\n\t/** The tool definitions. */\n\ttools: number;\n\t/**\n\t * The text of the user's prompts and the model's answers, and the provider's summary of the\n\t * turns before it once it has compacted them: only what the call sent.\n\t */\n\tmessages: number;\n\t/** Reasoning the model produced earlier and is sent back: its provider's own signed or encrypted blocks. */\n\treasoning: number;\n\t/** The input of every tool call so far. */\n\ttoolCalls: number;\n\t/** Every tool result so far, by tool, largest first. */\n\ttoolResults: { tool: string; characters: number; calls: number }[];\n\t/** The largest single results, each with its call's summary when the tool gives one. */\n\tlargestResults: { tool: string; characters: number; summary?: string }[];\n}\n\n/** A role in the fixed planner → executor → reviewer workflow. */\nexport type ProgressAgent = 'planner' | 'executor' | 'reviewer';\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError`, or the `TimeoutError` of\n * `AbortSignal.timeout`, must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AA+EA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\nimport type { Inbox } from './inbox.ts';\nimport type { Asker } from './question.ts';\nimport type { Session } from '#src/shared/domain/session';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n\t/**\n\t * The run the workspace's history recorded, when this run wrote through the harness's write\n\t * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes\n\t * the history does not record.\n\t */\n\trunId?: string;\n\t/**\n\t * The session the run belonged to: the one `RunOptions.session` gave, or the agent's own,\n\t * which the harness creates for each agent. Pass it to later runs, on this agent or one built\n\t * again, to keep them in the same session. Every agent the harness builds sets it.\n\t */\n\tsession?: Session;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/**\n * What a run needs besides its prompt. InstructedAgent passes it on whole. OrchestratorAgent\n * copies its own enumerable fields, replaces `onProgress` per role and drops `inbox`;\n * RetryingAgent does the same for a run with `ask`, replacing only `ask`. A field must therefore\n * be an own enumerable one to reach every agent.\n */\nexport interface RunOptions {\n\t/**\n\t * Cancels the run, which then rejects with the signal's reason: an `AbortError`, or a\n\t * `TimeoutError` from `AbortSignal.timeout`. `isAbortError` recognizes both.\n\t */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide. Agents a delegation runs at once may call it before\n\t * an earlier call has its answer.\n\t */\n\tapprove?: Approver | undefined;\n\t/**\n\t * Answers what a model-backed agent asks the user, through its `askUser` tool, when a\n\t * decision is the user's to make. Without it the agent has no such tool. The Agent SDK\n\t * engines ignore it. Agents a delegation runs at once may call it before an earlier call has\n\t * its answer.\n\t */\n\task?: Asker | undefined;\n\t/**\n\t * Messages sent to the run while it works. A model-backed agent reads them once the step in\n\t * progress ends, as a message from the user, and wakes for them while it waits for agents in\n\t * the background. The Agent SDK engines, the orchestrators and the workflows ignore it, so\n\t * `Inbox.close` returns everything sent to them.\n\t */\n\tinbox?: Inbox | undefined;\n\t/**\n\t * The session this run belongs to, such as the API's, recorded with each run that writes.\n\t * Without it, the run belongs to the agent's own session, the same for each of its runs, which\n\t * `AgentResponse.session` returns. An agent another agent started belongs to the outer run's.\n\t */\n\tsession?: Session | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number; agent?: ProgressAgent }\n\t| { type: 'reasoning'; message: string; agent?: ProgressAgent }\n\t| { type: 'search'; query: string; agent?: ProgressAgent }\n\t| {\n\t\t\ttype: 'fileChange';\n\t\t\tchanges: { path: string; kind: 'add' | 'update' | 'delete' }[];\n\t\t\tagent?: ProgressAgent;\n\t }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string; agent?: ProgressAgent }\n\t| { type: 'agentMessage'; message: string; agent?: ProgressAgent }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tagent?: ProgressAgent;\n\t\t\t/** A short, tool-owned description of the call. Narration only. */\n\t\t\tsummary?: string;\n\t\t\t/** The exit code of a configured command that ended with an error. Narration only. */\n\t\t\texitCode?: number;\n\t\t\t/**\n\t\t\t * On `error`, for an agent on the model APIs: what the model was told went wrong, such\n\t\t\t * as a patch that did not apply or a failed command's output. Narration only.\n\t\t\t */\n\t\t\terror?: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it. `sent` when\n\t\t\t * it sent subagents to work in the background, such as an `explore` call: they go on\n\t\t\t * after it, as `subagent` events, until a `subagentEnded` for each.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied' | 'sent';\n\t\t\t/**\n\t\t\t * On `completed`, for a call that changed a file through the harness's write tools: the\n\t\t\t * change as a unified diff, for the person watching. Narration only; the run's whole\n\t\t\t * record is the history's `changes`.\n\t\t\t */\n\t\t\tdiff?: string;\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[]; agent?: ProgressAgent }\n\t| { type: 'turnStarted'; agent?: ProgressAgent }\n\t| { type: 'turnEnded'; agent?: ProgressAgent }\n\t/**\n\t * One billed call to the model by an agent on the model APIs, including one the model\n\t * stopped early or the client turned into an error: what the call sent and what it cost.\n\t * Narration only: the run's total is `AgentResponse.tokens`, or the failure's `tokens`.\n\t */\n\t| {\n\t\t\ttype: 'step';\n\t\t\t/** 1 for the first call of the run. */\n\t\t\tstep: number;\n\t\t\t/** Missing when the provider did not report this call's usage. */\n\t\t\ttokens?: Tokens;\n\t\t\tcontext: StepContext;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * The context an agent on the model APIs sends was compacted: summarized from some point back,\n\t * while its conversation keeps every part. `by` is who compacted it, the model's provider or\n\t * the harness. `reason` is `threshold` when the context passed the agent's `compactAt`, and\n\t * `maxContext` when it no longer fit the model, which compacts it even with compaction off.\n\t * `contextTokens` is how many tokens of context were compacted, when known. The provider's\n\t * compaction follows the `step` event of the call that compacted, whose tokens include the\n\t * compaction's. The harness's is narrated once its summary is written, before the next call's\n\t * `step`: the summary call has no `step` of its own, and its tokens are only in the run's.\n\t */\n\t| {\n\t\t\ttype: 'compaction';\n\t\t\tby: 'provider' | 'harness';\n\t\t\treason: 'threshold' | 'maxContext';\n\t\t\tcontextTokens?: number;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A workflow's stage asks its agent for an answer; `attempt` is 2 or more when the previous\n\t * answer was invalid. Stages run one at a time, so the events that follow, until the next\n\t * workflow event, are this stage's agent's.\n\t */\n\t| { type: 'workflowStage'; stage: string; attempt: number; agent?: ProgressAgent }\n\t/** The transition a stage's valid answer selected, and the stage it leads to. */\n\t| {\n\t\t\ttype: 'workflowTransition';\n\t\t\tstage: string;\n\t\t\ttransition: 'go' | 'stop' | 'complete';\n\t\t\t/** Absent after `stop` or `complete`. */\n\t\t\tnextStage?: string;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * An event of a subagent that a tool call started: one of the explorers an `explore` call\n\t * sends, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that\n\t * call's `tool` events, and `subagent` numbers its subagents from 1, so the events of agents\n\t * running in parallel stay apart. Subagents work in the background: the call's own `tool`\n\t * event ends with `status: 'sent'`, and each subagent's events keep arriving after it, until\n\t * its `subagentEnded`, whether it answered, failed or was stopped. A subagent's own subagents\n\t * arrive as `subagent` events inside this one's `event`.\n\t */\n\t| {\n\t\t\ttype: 'subagent';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\t/** Which agent it is: `explorer`, or the name of the tool that delegates to it. */\n\t\t\tname: string;\n\t\t\t/** What the subagent was asked. */\n\t\t\ttask: string;\n\t\t\tevent: ProgressEvent;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A subagent that a call with `status: 'sent'` sent has ended: it `answered`, it `failed` and\n\t * its answer says why, or it was `stopped` because the run ended. `callId`, `subagent` and\n\t * `name` are those of its `subagent` events. Narration only: its answer reaches the model that\n\t * sent it, not the consumer.\n\t */\n\t| {\n\t\t\ttype: 'subagentEnded';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\tname: string;\n\t\t\toutcome: 'answered' | 'failed' | 'stopped';\n\t\t\tagent?: ProgressAgent;\n\t };\n\n/**\n * What one call to the model sent, in characters by where it came from: an estimate of size\n * for finding what fills a context, not a token count.\n */\nexport interface StepContext {\n\t/** The system prompt. */\n\tinstructions: number;\n\t/** The tool definitions. */\n\ttools: number;\n\t/**\n\t * The text of the user's prompts and the model's answers, and the provider's summary of the\n\t * turns before it once it has compacted them: only what the call sent.\n\t */\n\tmessages: number;\n\t/** Reasoning the model produced earlier and is sent back: its provider's own signed or encrypted blocks. */\n\treasoning: number;\n\t/** The input of every tool call so far. */\n\ttoolCalls: number;\n\t/** Every tool result so far, by tool, largest first. */\n\ttoolResults: { tool: string; characters: number; calls: number }[];\n\t/** The largest single results, each with its call's summary when the tool gives one. */\n\tlargestResults: { tool: string; characters: number; summary?: string }[];\n}\n\n/** A role in the fixed planner → executor → reviewer workflow. */\nexport type ProgressAgent = 'planner' | 'executor' | 'reviewer';\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError`, or the `TimeoutError` of\n * `AbortSignal.timeout`, must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
|
|
@@ -12,6 +12,24 @@ export interface CommandLine {
|
|
|
12
12
|
/** Where it runs, as a real path. */
|
|
13
13
|
cwd: string;
|
|
14
14
|
}
|
|
15
|
+
/** A worktree a tool call will remove, and what is lost with it. */
|
|
16
|
+
export interface WorktreeRemoval {
|
|
17
|
+
/** Its folder, as a real path. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** The branch it has checked out, which stays with its commits; absent when detached. */
|
|
20
|
+
branch?: string;
|
|
21
|
+
/** How many files hold changes no commit has, untracked ones included: they are lost. */
|
|
22
|
+
uncommitted: number;
|
|
23
|
+
/**
|
|
24
|
+
* How many paths `.gitignore` excludes, such as `.env` or `node_modules`, a folder excluded
|
|
25
|
+
* whole counting once: they are lost too. Such a folder is never looked into, so what is
|
|
26
|
+
* written in it while the user is asked goes with it unannounced; a host's question says so
|
|
27
|
+
* when there are any.
|
|
28
|
+
*/
|
|
29
|
+
ignored: number;
|
|
30
|
+
/** With a detached HEAD, how many of its commits no branch, tag or remote holds: lost too. */
|
|
31
|
+
commitsOnlyHere: number;
|
|
32
|
+
}
|
|
15
33
|
/** A tool call waiting for someone to allow it. */
|
|
16
34
|
export interface ApprovalRequest {
|
|
17
35
|
tool: string;
|
|
@@ -20,6 +38,8 @@ export interface ApprovalRequest {
|
|
|
20
38
|
risk: ToolRisk;
|
|
21
39
|
/** For a call that runs a configured command: the program, worked out from the input. */
|
|
22
40
|
command?: CommandLine;
|
|
41
|
+
/** For a call that removes a worktree: which one, and what is lost with it. */
|
|
42
|
+
worktree?: WorktreeRemoval;
|
|
23
43
|
}
|
|
24
44
|
/** A denial's `reason` reaches the model, so it can try something else. */
|
|
25
45
|
export type ApprovalDecision = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"AAkEA;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAChC,GAGyD,EACzD,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAqD,EAAE;IAEtF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QACxB,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAE/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1C,0FAA0F;QAC1F,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,8FAA8F;QAC9F,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACxC,IAAI,YAAY,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,OAAO,QAAQ,CAAC;IACjB,CAAC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAe;IAC3C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAkD,CAAC;IAChF,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,QAAQ;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,YAAY,CAAC,MAAe;IACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAgC,CAAC;IACtD,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC7D,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,QAAQ,KAAK,IAAI,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * How much one tool call can hurt, judged by the tool from that call's input:\n * - `safe` changes nothing, as with a read.\n * - `mutating` makes a change git can undo, as with creating a file.\n * - `destructive` loses something, as with overwriting or deleting a file.\n */\nexport type ToolRisk = 'safe' | 'mutating' | 'destructive';\n\n/** A program a tool call will run, exactly as it will run: no shell reads it. */\nexport interface CommandLine {\n\texecutable: string;\n\targs: string[];\n\t/** Where it runs, as a real path. */\n\tcwd: string;\n}\n\n/** A worktree a tool call will remove, and what is lost with it. */\nexport interface WorktreeRemoval {\n\t/** Its folder, as a real path. */\n\tpath: string;\n\t/** The branch it has checked out, which stays with its commits; absent when detached. */\n\tbranch?: string;\n\t/** How many files hold changes no commit has, untracked ones included: they are lost. */\n\tuncommitted: number;\n\t/**\n\t * How many paths `.gitignore` excludes, such as `.env` or `node_modules`, a folder excluded\n\t * whole counting once: they are lost too. Such a folder is never looked into, so what is\n\t * written in it while the user is asked goes with it unannounced; a host's question says so\n\t * when there are any.\n\t */\n\tignored: number;\n\t/** With a detached HEAD, how many of its commits no branch, tag or remote holds: lost too. */\n\tcommitsOnlyHere: number;\n}\n\n/** A tool call waiting for someone to allow it. */\nexport interface ApprovalRequest {\n\ttool: string;\n\t/** What the model sent, unvalidated: the tool validates it only if the call is allowed. */\n\tinput: unknown;\n\trisk: ToolRisk;\n\t/** For a call that runs a configured command: the program, worked out from the input. */\n\tcommand?: CommandLine;\n\t/** For a call that removes a worktree: which one, and what is lost with it. */\n\tworktree?: WorktreeRemoval;\n}\n\n/** A denial's `reason` reaches the model, so it can try something else. */\nexport type ApprovalDecision =\n\t{ approved: true } | { approved: false; reason?: string | undefined };\n\n/**\n * Decides one tool call at a time. A consumer that wants \"don't ask again\" remembers its own\n * answers, so the harness keeps no session state. A throw ends the run as an\n * `UnrecoverableError`.\n */\nexport type Approver = (\n\trequest: ApprovalRequest,\n\tsignal: AbortSignal,\n) => ApprovalDecision | Promise<ApprovalDecision>;\n\n/** What `rememberApprovals` asks for: `remember` allows the same call again without asking. */\nexport type RememberableDecision =\n\t| { approved: true; remember?: boolean | undefined }\n\t| { approved: false; reason?: string | undefined };\n\n/**\n * Wraps `ask` so a call the user allowed with `remember` is not asked about again. `key` decides\n * what counts as the same call; it defaults to the tool's name, so \"always\" allows every\n * destructive call to that tool. The memory lives in the returned approver, so its lifetime is\n * the consumer's choice: one per session, one per user, or one per run. Calls made at once are\n * each asked; a call that waits its turn in front of it finds an \"always\" given meanwhile.\n */\nexport function rememberApprovals(\n\task: (\n\t\trequest: ApprovalRequest,\n\t\tsignal: AbortSignal,\n\t) => RememberableDecision | Promise<RememberableDecision>,\n\t{ key = request => request.tool }: { key?: (request: ApprovalRequest) => string } = {},\n): Approver {\n\tconst allowed = new Set<string>();\n\n\treturn async (request, signal) => {\n\t\tconst id = key(request);\n\t\tif (allowed.has(id)) return { approved: true };\n\n\t\tconst answer = await ask(request, signal);\n\t\t// The agent discards an answer that arrives after a cancellation, so it must not be kept.\n\t\tsignal.throwIfAborted();\n\t\t// Read before keeping anything: a truthy `'false'` would otherwise be allowed and remembered.\n\t\tconst decision = readDecision(answer);\n\t\tif (!decision.approved) return decision;\n\t\tif (readRemember(answer)) allowed.add(id);\n\t\treturn decision;\n\t};\n}\n\n/**\n * Reads an approver's answer without trusting its type, since a JavaScript consumer can return\n * anything: only an `approved` that is a boolean decides, and anything else throws.\n */\nexport function readDecision(answer: unknown): ApprovalDecision {\n\tif (typeof answer !== 'object' || answer === null) {\n\t\tthrow new TypeError('The approver answered without a decision');\n\t}\n\tconst { approved, reason } = answer as { approved?: unknown; reason?: unknown };\n\tif (typeof approved !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `approved`');\n\t}\n\n\treturn approved\n\t\t? { approved }\n\t\t: { approved, reason: typeof reason === 'string' ? reason : undefined };\n}\n\nfunction readRemember(answer: unknown): boolean {\n\tconst { remember } = answer as { remember?: unknown };\n\tif (remember !== undefined && typeof remember !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `remember`');\n\t}\n\treturn remember === true;\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages a consumer sends to a run while it works, such as what the user typed meanwhile and
|
|
3
|
+
* asked to have read at once. A model-backed agent reads them once the step in progress ends,
|
|
4
|
+
* and wakes for them while it waits for the agents it sent to work in the background. The Agent
|
|
5
|
+
* SDK engines, the orchestrators and the workflows never read them.
|
|
6
|
+
*
|
|
7
|
+
* One inbox belongs to one run: create it for the run, pass it in `RunOptions`, and close it
|
|
8
|
+
* once the run ends.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Inbox {
|
|
11
|
+
constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Sends a message to the run. False once the inbox is closed, whatever the text: no run will
|
|
14
|
+
* read it.
|
|
15
|
+
*
|
|
16
|
+
* @throws {RangeError} when the inbox is open and `text` holds nothing but whitespace, which a
|
|
17
|
+
* provider may refuse.
|
|
18
|
+
*/
|
|
19
|
+
send(text: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Closes the inbox, and returns every message the run did not keep, in the order they were
|
|
22
|
+
* sent: those it never read, such as one sent after its final answer, and those it read in an
|
|
23
|
+
* attempt that failed. They belong to the next run.
|
|
24
|
+
*/
|
|
25
|
+
close(): string[];
|
|
26
|
+
}
|
|
27
|
+
/** How a run reads its inbox. Internal. */
|
|
28
|
+
export interface InboxReader {
|
|
29
|
+
/** Takes every message waiting, in the order they were sent. */
|
|
30
|
+
take(): string[];
|
|
31
|
+
/** Gives back messages a run read but did not keep, ahead of any sent since. */
|
|
32
|
+
giveBack(messages: readonly string[]): void;
|
|
33
|
+
/** Calls `listener` for each message sent, until the returned function is called. */
|
|
34
|
+
onMessage(listener: () => void): () => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* How a run reads `inbox`, which comes untyped from a consumer's options.
|
|
38
|
+
*
|
|
39
|
+
* @throws {UnrecoverableError} for anything that is not an `Inbox` of this package, such as a
|
|
40
|
+
* plain object, or one made by another copy of the package.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readerOf(inbox: unknown): InboxReader;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { UnrecoverableError } from '#src/shared/domain/errors';
|
|
2
|
+
/**
|
|
3
|
+
* Messages a consumer sends to a run while it works, such as what the user typed meanwhile and
|
|
4
|
+
* asked to have read at once. A model-backed agent reads them once the step in progress ends,
|
|
5
|
+
* and wakes for them while it waits for the agents it sent to work in the background. The Agent
|
|
6
|
+
* SDK engines, the orchestrators and the workflows never read them.
|
|
7
|
+
*
|
|
8
|
+
* One inbox belongs to one run: create it for the run, pass it in `RunOptions`, and close it
|
|
9
|
+
* once the run ends.
|
|
10
|
+
*/
|
|
11
|
+
export class Inbox {
|
|
12
|
+
constructor() {
|
|
13
|
+
states.set(this, { waiting: [], closed: false, listeners: new Set() });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Sends a message to the run. False once the inbox is closed, whatever the text: no run will
|
|
17
|
+
* read it.
|
|
18
|
+
*
|
|
19
|
+
* @throws {RangeError} when the inbox is open and `text` holds nothing but whitespace, which a
|
|
20
|
+
* provider may refuse.
|
|
21
|
+
*/
|
|
22
|
+
send(text) {
|
|
23
|
+
const state = stateOf(this);
|
|
24
|
+
if (state.closed)
|
|
25
|
+
return false;
|
|
26
|
+
if (text.trim() === '')
|
|
27
|
+
throw new RangeError('A message to a run needs some text');
|
|
28
|
+
state.waiting.push(text);
|
|
29
|
+
for (const listener of state.listeners)
|
|
30
|
+
listener();
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Closes the inbox, and returns every message the run did not keep, in the order they were
|
|
35
|
+
* sent: those it never read, such as one sent after its final answer, and those it read in an
|
|
36
|
+
* attempt that failed. They belong to the next run.
|
|
37
|
+
*/
|
|
38
|
+
close() {
|
|
39
|
+
const state = stateOf(this);
|
|
40
|
+
state.closed = true;
|
|
41
|
+
state.listeners.clear();
|
|
42
|
+
return state.waiting.splice(0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Kept apart from the class, so a consumer sees only `send` and `close`.
|
|
46
|
+
const states = new WeakMap();
|
|
47
|
+
function stateOf(inbox) {
|
|
48
|
+
const state = states.get(inbox);
|
|
49
|
+
if (!state)
|
|
50
|
+
throw new TypeError('Not an Inbox');
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* How a run reads `inbox`, which comes untyped from a consumer's options.
|
|
55
|
+
*
|
|
56
|
+
* @throws {UnrecoverableError} for anything that is not an `Inbox` of this package, such as a
|
|
57
|
+
* plain object, or one made by another copy of the package.
|
|
58
|
+
*/
|
|
59
|
+
export function readerOf(inbox) {
|
|
60
|
+
const state = typeof inbox === 'object' && inbox !== null ? states.get(inbox) : undefined;
|
|
61
|
+
if (!state) {
|
|
62
|
+
throw new UnrecoverableError('The run was given an inbox that is not an Inbox', {
|
|
63
|
+
cause: 'RunOptions.inbox, when given, is an Inbox this package made.',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
take: () => state.waiting.splice(0),
|
|
68
|
+
giveBack: messages => {
|
|
69
|
+
state.waiting.unshift(...messages);
|
|
70
|
+
},
|
|
71
|
+
onMessage: listener => {
|
|
72
|
+
if (state.closed)
|
|
73
|
+
return () => undefined;
|
|
74
|
+
state.listeners.add(listener);
|
|
75
|
+
return () => {
|
|
76
|
+
state.listeners.delete(listener);
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../../src/agent/domain/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,OAAO,KAAK;IACjB;QACC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IACxE,CAAC;IAED;;;;;;OAMG;IACH,IAAI,CAAC,IAAY;QAChB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC/B,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,MAAM,IAAI,UAAU,CAAC,oCAAoC,CAAC,CAAC;QACnF,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,SAAS;YAAE,QAAQ,EAAE,CAAC;QACnD,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;OAIG;IACH,KAAK;QACJ,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;CACD;AAQD,yEAAyE;AACzE,MAAM,MAAM,GAAG,IAAI,OAAO,EAAqB,CAAC;AAEhD,SAAS,OAAO,CAAC,KAAY;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,SAAS,CAAC,cAAc,CAAC,CAAC;IAChD,OAAO,KAAK,CAAC;AACd,CAAC;AAYD;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc;IACtC,MAAM,KAAK,GACV,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAc,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACtF,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,kBAAkB,CAAC,iDAAiD,EAAE;YAC/E,KAAK,EAAE,8DAA8D;SACrE,CAAC,CAAC;IACJ,CAAC;IACD,OAAO;QACN,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACnC,QAAQ,EAAE,QAAQ,CAAC,EAAE;YACpB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;QACpC,CAAC;QACD,SAAS,EAAE,QAAQ,CAAC,EAAE;YACrB,IAAI,KAAK,CAAC,MAAM;gBAAE,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;YACzC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC9B,OAAO,GAAG,EAAE;gBACX,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAClC,CAAC,CAAC;QACH,CAAC;KACD,CAAC;AACH,CAAC","sourcesContent":["import { UnrecoverableError } from '#src/shared/domain/errors';\n\n/**\n * Messages a consumer sends to a run while it works, such as what the user typed meanwhile and\n * asked to have read at once. A model-backed agent reads them once the step in progress ends,\n * and wakes for them while it waits for the agents it sent to work in the background. The Agent\n * SDK engines, the orchestrators and the workflows never read them.\n *\n * One inbox belongs to one run: create it for the run, pass it in `RunOptions`, and close it\n * once the run ends.\n */\nexport class Inbox {\n\tconstructor() {\n\t\tstates.set(this, { waiting: [], closed: false, listeners: new Set() });\n\t}\n\n\t/**\n\t * Sends a message to the run. False once the inbox is closed, whatever the text: no run will\n\t * read it.\n\t *\n\t * @throws {RangeError} when the inbox is open and `text` holds nothing but whitespace, which a\n\t * provider may refuse.\n\t */\n\tsend(text: string): boolean {\n\t\tconst state = stateOf(this);\n\t\tif (state.closed) return false;\n\t\tif (text.trim() === '') throw new RangeError('A message to a run needs some text');\n\t\tstate.waiting.push(text);\n\t\tfor (const listener of state.listeners) listener();\n\t\treturn true;\n\t}\n\n\t/**\n\t * Closes the inbox, and returns every message the run did not keep, in the order they were\n\t * sent: those it never read, such as one sent after its final answer, and those it read in an\n\t * attempt that failed. They belong to the next run.\n\t */\n\tclose(): string[] {\n\t\tconst state = stateOf(this);\n\t\tstate.closed = true;\n\t\tstate.listeners.clear();\n\t\treturn state.waiting.splice(0);\n\t}\n}\n\ninterface InboxState {\n\twaiting: string[];\n\tclosed: boolean;\n\tlisteners: Set<() => void>;\n}\n\n// Kept apart from the class, so a consumer sees only `send` and `close`.\nconst states = new WeakMap<Inbox, InboxState>();\n\nfunction stateOf(inbox: Inbox): InboxState {\n\tconst state = states.get(inbox);\n\tif (!state) throw new TypeError('Not an Inbox');\n\treturn state;\n}\n\n/** How a run reads its inbox. Internal. */\nexport interface InboxReader {\n\t/** Takes every message waiting, in the order they were sent. */\n\ttake(): string[];\n\t/** Gives back messages a run read but did not keep, ahead of any sent since. */\n\tgiveBack(messages: readonly string[]): void;\n\t/** Calls `listener` for each message sent, until the returned function is called. */\n\tonMessage(listener: () => void): () => void;\n}\n\n/**\n * How a run reads `inbox`, which comes untyped from a consumer's options.\n *\n * @throws {UnrecoverableError} for anything that is not an `Inbox` of this package, such as a\n * plain object, or one made by another copy of the package.\n */\nexport function readerOf(inbox: unknown): InboxReader {\n\tconst state =\n\t\ttypeof inbox === 'object' && inbox !== null ? states.get(inbox as Inbox) : undefined;\n\tif (!state) {\n\t\tthrow new UnrecoverableError('The run was given an inbox that is not an Inbox', {\n\t\t\tcause: 'RunOptions.inbox, when given, is an Inbox this package made.',\n\t\t});\n\t}\n\treturn {\n\t\ttake: () => state.waiting.splice(0),\n\t\tgiveBack: messages => {\n\t\t\tstate.waiting.unshift(...messages);\n\t\t},\n\t\tonMessage: listener => {\n\t\t\tif (state.closed) return () => undefined;\n\t\t\tstate.listeners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tstate.listeners.delete(listener);\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { UnrecoverableError } from '#src/shared/domain/errors';
|
|
2
|
+
type Options = ConstructorParameters<typeof UnrecoverableError>[1];
|
|
3
|
+
/**
|
|
4
|
+
* A run started on an agent instance while another run on it works. An instance continues one
|
|
5
|
+
* conversation, so two runs at once would each continue it from the same point, and one would
|
|
6
|
+
* silently drop the other.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RunInProgressError extends UnrecoverableError {
|
|
9
|
+
constructor(message?: string, options?: Options);
|
|
10
|
+
}
|
|
11
|
+
/** Lets an agent instance work on one run at a time, and rejects a run that starts meanwhile. */
|
|
12
|
+
export declare class OneRunAtATime {
|
|
13
|
+
private running;
|
|
14
|
+
/** Runs `work`, unless a run is already in progress. */
|
|
15
|
+
run<T>(work: () => Promise<T>): Promise<T>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|