@germanescobar/anita 0.3.0 → 0.4.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 +33 -0
- package/dist/agent/context-builder.d.ts +1 -4
- package/dist/agent/context-builder.js +30 -35
- package/dist/agent/executor.d.ts +21 -2
- package/dist/agent/executor.js +52 -2
- package/dist/agent/loop.d.ts +8 -0
- package/dist/agent/loop.js +66 -4
- package/dist/cli/index.d.ts +48 -0
- package/dist/cli/index.js +173 -9
- package/dist/models/resolve.d.ts +1 -1
- package/dist/models/resolve.js +35 -27
- package/dist/types/approval.d.ts +55 -0
- package/dist/types/approval.js +9 -0
- package/dist/types/events.d.ts +1 -1
- package/dist/types/stream.d.ts +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -200,6 +200,39 @@ Example stream:
|
|
|
200
200
|
{"type":"run.completed","sessionId":"9e6f8a7d-7ff1-4c2c-b3d8-9c3ed5a1d4b7","status":"completed","stopReason":"end_turn","timestamp":"2026-04-09T15:00:01.000Z"}
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
#### Approvals in `--stream-json` mode
|
|
204
|
+
|
|
205
|
+
When a tool call requires approval (policy returns `ask`), the CLI emits two
|
|
206
|
+
extra events around the gate so consumers can audit decisions. They fire in
|
|
207
|
+
every mode — including `--auto-approve` and human TTY mode — so the stream
|
|
208
|
+
shape is uniform regardless of how the answer was produced.
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{"type":"approval.request","id":"toolu_123","tool":"run_command","input":{"command":"npm test"},"timestamp":"…"}
|
|
212
|
+
{"type":"approval.resolved","id":"toolu_123","approved":true,"reason":"user","timestamp":"…"}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`id` is the model's tool call id and matches the surrounding `tool.call` /
|
|
216
|
+
`tool.result` lifecycle. `input` is the structured object, never a pre-rendered
|
|
217
|
+
string. `reason` is one of:
|
|
218
|
+
|
|
219
|
+
- `user` — the responder answered.
|
|
220
|
+
- `aborted` — the run's `AbortSignal` fired while waiting.
|
|
221
|
+
- `eof` — the consumer closed stdin before answering.
|
|
222
|
+
- `error` — the approval callback itself threw.
|
|
223
|
+
|
|
224
|
+
When stdin is a pipe, the CLI never writes a prompt to stdout or stderr. The
|
|
225
|
+
consumer answers by writing a single JSON line to stdin:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{"type":"approval.response","id":"toolu_123","approved":true}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The resolver waits silently on stdin. Mismatched `id`s, malformed JSON, and
|
|
232
|
+
unknown message types are discarded (logged to stderr) so a stale response
|
|
233
|
+
can't poison the run. There is exactly one approval pending at a time — the
|
|
234
|
+
executor runs tool calls sequentially.
|
|
235
|
+
|
|
203
236
|
Without `--stream-json`, the CLI uses the normal human-readable terminal output.
|
|
204
237
|
|
|
205
238
|
When using an OpenAI-compatible backend that exposes reasoning traces, Anita will also store them in the `assistant_response` event payload as `reasoning` and emit an `assistant.reasoning` stream event.
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { ApprovalMode, PolicyContext } from "./policies.js";
|
|
2
|
-
import type { ConversationItem } from "../types/conversation.js";
|
|
3
|
-
import type { Message } from "../types/messages.js";
|
|
4
2
|
import type { Skill } from "../skills/skills.js";
|
|
5
3
|
export type NetworkAccess = "available" | "unavailable" | "unknown";
|
|
6
4
|
export interface RuntimeContextOptions {
|
|
@@ -19,11 +17,10 @@ export declare class ContextBuilder {
|
|
|
19
17
|
private runtimeContext;
|
|
20
18
|
constructor(workingDirectory: string, runtimeContext?: RuntimeContextOptions);
|
|
21
19
|
buildSystemPrompt(): string;
|
|
20
|
+
appendRuntimeContext(baseSystemPrompt: string): Promise<string>;
|
|
22
21
|
private buildUserSystemPromptSection;
|
|
23
22
|
private buildEnvironmentFooter;
|
|
24
23
|
buildDynamicContext(): Promise<string>;
|
|
25
|
-
buildMessagesWithDynamicContext(messages: Message[]): Promise<Message[]>;
|
|
26
|
-
buildItemsWithDynamicContext(items: ConversationItem[]): Promise<ConversationItem[]>;
|
|
27
24
|
private buildEnvironmentContext;
|
|
28
25
|
private buildPolicyContext;
|
|
29
26
|
private describeApprovalMode;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
2
|
import { formatSkillsForPrompt } from "../skills/skills.js";
|
|
3
3
|
import { formatAgentInstructionsForPrompt, loadAgentInstructions, } from "./agents.js";
|
|
4
|
-
const STATIC_SYSTEM_PROMPT = `You are Anita,
|
|
4
|
+
const STATIC_SYSTEM_PROMPT = `You are Anita, an extremely capable coding agent.
|
|
5
5
|
|
|
6
6
|
You have these tools available:
|
|
7
7
|
- read_file: Read file contents
|
|
@@ -11,13 +11,14 @@ You have these tools available:
|
|
|
11
11
|
- run_command: Run a shell command
|
|
12
12
|
|
|
13
13
|
Instructions:
|
|
14
|
-
- Use tools to explore and understand the codebase before making changes
|
|
15
|
-
- Prefer rg for repository text searches when available; fall back to grep only when rg is unavailable
|
|
16
|
-
- Always read a file before editing it
|
|
17
|
-
- Run tests or checks after making changes when appropriate
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
14
|
+
- Use tools to explore and understand the codebase before making changes.
|
|
15
|
+
- Prefer rg for repository text searches when available; fall back to grep only when rg is unavailable.
|
|
16
|
+
- Always read a file before editing it.
|
|
17
|
+
- Run tests or checks after making changes when appropriate.
|
|
18
|
+
- Do not print or publish secrets, credentials, tokens, private keys, environment values, or other sensitive data.
|
|
19
|
+
- Explain what you are doing briefly.
|
|
20
|
+
- Sometimes the most simple solution is the correct one. Try it first and ask the user before jumping ahead or going deeper. Don't spiral into rabbit holes.
|
|
21
|
+
- Always verify, don't guess.`;
|
|
21
22
|
export class ContextBuilder {
|
|
22
23
|
workingDirectory;
|
|
23
24
|
runtimeContext;
|
|
@@ -33,11 +34,30 @@ export class ContextBuilder {
|
|
|
33
34
|
repositoryRoot: this.runtimeContext.agentsRepositoryRoot,
|
|
34
35
|
}));
|
|
35
36
|
return (STATIC_SYSTEM_PROMPT +
|
|
37
|
+
skillsSection +
|
|
36
38
|
agentInstructionsSection +
|
|
37
39
|
this.buildUserSystemPromptSection() +
|
|
38
|
-
skillsSection +
|
|
39
40
|
this.buildEnvironmentFooter());
|
|
40
41
|
}
|
|
42
|
+
/*
|
|
43
|
+
* Appends the dynamic runtime context to an already-built base system prompt.
|
|
44
|
+
*
|
|
45
|
+
* Runtime context (environment, policy, git state) belongs in the system
|
|
46
|
+
* prompt rather than as a separate user turn. Injecting it as a user message
|
|
47
|
+
* placed it adjacent to the real request, and weaker models conflated the two
|
|
48
|
+
* and treated the actual request as context.
|
|
49
|
+
*
|
|
50
|
+
* The caller passes the base prompt so it can be built once per run: rebuilding
|
|
51
|
+
* it each turn would reload AGENTS.md from disk and let a tool that writes
|
|
52
|
+
* AGENTS.md inject generated text as system instructions mid-run. Only the
|
|
53
|
+
* runtime context is refreshed here so git state stays current.
|
|
54
|
+
*/
|
|
55
|
+
async appendRuntimeContext(baseSystemPrompt) {
|
|
56
|
+
const dynamicContext = await this.buildDynamicContext();
|
|
57
|
+
if (!dynamicContext)
|
|
58
|
+
return baseSystemPrompt;
|
|
59
|
+
return `${baseSystemPrompt}\n\n${dynamicContext}`;
|
|
60
|
+
}
|
|
41
61
|
buildUserSystemPromptSection() {
|
|
42
62
|
const systemPrompt = this.runtimeContext.systemPrompt?.trim();
|
|
43
63
|
if (!systemPrompt)
|
|
@@ -63,7 +83,7 @@ export class ContextBuilder {
|
|
|
63
83
|
const environmentContext = this.buildEnvironmentContext();
|
|
64
84
|
const policyContext = this.buildPolicyContext();
|
|
65
85
|
return [
|
|
66
|
-
"Runtime context
|
|
86
|
+
"Runtime context (current environment state, refreshed each turn; not a user request):",
|
|
67
87
|
environmentContext,
|
|
68
88
|
policyContext,
|
|
69
89
|
gitContext,
|
|
@@ -71,31 +91,6 @@ export class ContextBuilder {
|
|
|
71
91
|
.filter(Boolean)
|
|
72
92
|
.join("\n\n");
|
|
73
93
|
}
|
|
74
|
-
async buildMessagesWithDynamicContext(messages) {
|
|
75
|
-
const dynamicContext = await this.buildDynamicContext();
|
|
76
|
-
if (!dynamicContext)
|
|
77
|
-
return messages;
|
|
78
|
-
return [
|
|
79
|
-
{
|
|
80
|
-
role: "user",
|
|
81
|
-
content: [{ type: "text", text: dynamicContext }],
|
|
82
|
-
},
|
|
83
|
-
...messages,
|
|
84
|
-
];
|
|
85
|
-
}
|
|
86
|
-
async buildItemsWithDynamicContext(items) {
|
|
87
|
-
const dynamicContext = await this.buildDynamicContext();
|
|
88
|
-
if (!dynamicContext)
|
|
89
|
-
return items;
|
|
90
|
-
return [
|
|
91
|
-
{
|
|
92
|
-
type: "message",
|
|
93
|
-
role: "user",
|
|
94
|
-
content: dynamicContext,
|
|
95
|
-
},
|
|
96
|
-
...items,
|
|
97
|
-
];
|
|
98
|
-
}
|
|
99
94
|
buildEnvironmentContext() {
|
|
100
95
|
const shell = this.runtimeContext.shell ?? process.env.SHELL ?? "unknown";
|
|
101
96
|
const approvalMode = this.runtimeContext.approvalMode ?? "prompt";
|
package/dist/agent/executor.d.ts
CHANGED
|
@@ -2,12 +2,31 @@ import type { ToolCall, ToolExecuteOptions, ToolResult } from "../types/tools.js
|
|
|
2
2
|
import type { ToolRegistry } from "../tools/registry.js";
|
|
3
3
|
import type { EventStore } from "../storage/event-store.js";
|
|
4
4
|
import type { PolicyEngine } from "./policies.js";
|
|
5
|
-
|
|
5
|
+
import type { ApprovalAnswer, ApprovalCallback, ApprovalNotifier, ApprovalRequest } from "../types/approval.js";
|
|
6
|
+
export type { ApprovalAnswer, ApprovalCallback, ApprovalNotifier, ApprovalRequest, };
|
|
6
7
|
export declare class Executor {
|
|
7
8
|
private registry;
|
|
8
9
|
private policyEngine;
|
|
9
10
|
private eventStore;
|
|
10
11
|
private approvalCallback;
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Optional notifier used to emit structured `approval.request` /
|
|
14
|
+
* `approval.resolved` stream events. Kept as an injected seam so the
|
|
15
|
+
* Executor does not depend on AgentLoop or stream-json mode.
|
|
16
|
+
*/
|
|
17
|
+
private approvalNotifier?;
|
|
18
|
+
constructor(registry: ToolRegistry, policyEngine: PolicyEngine, eventStore: EventStore, approvalCallback: ApprovalCallback,
|
|
19
|
+
/**
|
|
20
|
+
* Optional notifier used to emit structured `approval.request` /
|
|
21
|
+
* `approval.resolved` stream events. Kept as an injected seam so the
|
|
22
|
+
* Executor does not depend on AgentLoop or stream-json mode.
|
|
23
|
+
*/
|
|
24
|
+
approvalNotifier?: ApprovalNotifier | undefined);
|
|
25
|
+
/**
|
|
26
|
+
* Install (or replace) the approval notifier after construction. Used by
|
|
27
|
+
* `AgentLoop` to wire the executor into its own stream-json emitter without
|
|
28
|
+
* changing the public constructor order.
|
|
29
|
+
*/
|
|
30
|
+
setApprovalNotifier(notifier: ApprovalNotifier | undefined): void;
|
|
12
31
|
executeTool(sessionId: string, toolCall: ToolCall, options?: ToolExecuteOptions): Promise<ToolResult>;
|
|
13
32
|
}
|
package/dist/agent/executor.js
CHANGED
|
@@ -3,11 +3,27 @@ export class Executor {
|
|
|
3
3
|
policyEngine;
|
|
4
4
|
eventStore;
|
|
5
5
|
approvalCallback;
|
|
6
|
-
|
|
6
|
+
approvalNotifier;
|
|
7
|
+
constructor(registry, policyEngine, eventStore, approvalCallback,
|
|
8
|
+
/**
|
|
9
|
+
* Optional notifier used to emit structured `approval.request` /
|
|
10
|
+
* `approval.resolved` stream events. Kept as an injected seam so the
|
|
11
|
+
* Executor does not depend on AgentLoop or stream-json mode.
|
|
12
|
+
*/
|
|
13
|
+
approvalNotifier) {
|
|
7
14
|
this.registry = registry;
|
|
8
15
|
this.policyEngine = policyEngine;
|
|
9
16
|
this.eventStore = eventStore;
|
|
10
17
|
this.approvalCallback = approvalCallback;
|
|
18
|
+
this.approvalNotifier = approvalNotifier;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Install (or replace) the approval notifier after construction. Used by
|
|
22
|
+
* `AgentLoop` to wire the executor into its own stream-json emitter without
|
|
23
|
+
* changing the public constructor order.
|
|
24
|
+
*/
|
|
25
|
+
setApprovalNotifier(notifier) {
|
|
26
|
+
this.approvalNotifier = notifier;
|
|
11
27
|
}
|
|
12
28
|
async executeTool(sessionId, toolCall, options) {
|
|
13
29
|
const validationError = this.registry.validateInput(toolCall.name, toolCall.input);
|
|
@@ -39,7 +55,41 @@ export class Executor {
|
|
|
39
55
|
};
|
|
40
56
|
}
|
|
41
57
|
if (decision === "ask") {
|
|
42
|
-
const
|
|
58
|
+
const request = {
|
|
59
|
+
toolCallId: toolCall.id,
|
|
60
|
+
toolName: toolCall.name,
|
|
61
|
+
input: toolCall.input,
|
|
62
|
+
};
|
|
63
|
+
// Emit the request before awaiting the responder so consumers see
|
|
64
|
+
// approval.request ahead of any tool.call/tool.result events for the
|
|
65
|
+
// same toolCallId.
|
|
66
|
+
this.approvalNotifier?.notifyApprovalRequest(request);
|
|
67
|
+
let answer;
|
|
68
|
+
try {
|
|
69
|
+
answer = await this.approvalCallback(request, options?.signal);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
this.approvalNotifier?.notifyApprovalResolved({
|
|
73
|
+
...request,
|
|
74
|
+
approved: false,
|
|
75
|
+
reason: "error",
|
|
76
|
+
error: err instanceof Error ? err.message : String(err),
|
|
77
|
+
});
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
const approved = typeof answer === "boolean" ? answer : answer.approved;
|
|
81
|
+
// Resolver may supply a reason (e.g. stdin EOF). Otherwise fall back
|
|
82
|
+
// to "aborted" when the surrounding signal is already aborted, else
|
|
83
|
+
// "user".
|
|
84
|
+
const responderReason = typeof answer === "boolean" ? undefined : answer.reason;
|
|
85
|
+
const reason = options?.signal?.aborted
|
|
86
|
+
? "aborted"
|
|
87
|
+
: (responderReason ?? "user");
|
|
88
|
+
this.approvalNotifier?.notifyApprovalResolved({
|
|
89
|
+
...request,
|
|
90
|
+
approved,
|
|
91
|
+
reason,
|
|
92
|
+
});
|
|
43
93
|
if (!approved) {
|
|
44
94
|
return {
|
|
45
95
|
content: `Tool "${toolCall.name}" was denied by user.`,
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { SessionState } from "../types/agent.js";
|
|
|
6
6
|
import type { AttachmentContentBlock } from "../types/messages.js";
|
|
7
7
|
import { ContextBuilder } from "./context-builder.js";
|
|
8
8
|
import { Executor } from "./executor.js";
|
|
9
|
+
import type { ApprovalRequest, ApprovalResolvedEvent } from "../types/approval.js";
|
|
9
10
|
export declare const DEFAULT_CONTEXT_BUDGET: ContextBudgetOptions;
|
|
10
11
|
export interface ContextBudgetOptions {
|
|
11
12
|
compactAtRatio: number;
|
|
@@ -25,6 +26,13 @@ export declare class AgentLoop {
|
|
|
25
26
|
private contextBudget;
|
|
26
27
|
private pendingTerminalDelta;
|
|
27
28
|
constructor(provider: ModelProvider, executor: Executor, contextBuilder: ContextBuilder, registry: ToolRegistry, eventStore: EventStore, sessionStore: SessionStore, streamJson?: boolean, contextBudget?: ContextBudgetOptions);
|
|
29
|
+
/**
|
|
30
|
+
* ApprovalNotifier implementation: emits structured stream events around
|
|
31
|
+
* every approval gate so consumers can correlate the request with the
|
|
32
|
+
* matching tool.call / tool.result lifecycle.
|
|
33
|
+
*/
|
|
34
|
+
notifyApprovalRequest(request: ApprovalRequest): void;
|
|
35
|
+
notifyApprovalResolved(event: ApprovalResolvedEvent): void;
|
|
28
36
|
run(session: SessionState, userMessage: string, attachments?: AttachmentContentBlock[], signal?: AbortSignal): Promise<void>;
|
|
29
37
|
/**
|
|
30
38
|
* Record a coherent cancellation: persist the session at its last consistent
|
package/dist/agent/loop.js
CHANGED
|
@@ -34,6 +34,36 @@ export class AgentLoop {
|
|
|
34
34
|
this.sessionStore = sessionStore;
|
|
35
35
|
this.streamJson = streamJson;
|
|
36
36
|
this.contextBudget = contextBudget;
|
|
37
|
+
// Route executor approval events through this loop's stream emitter.
|
|
38
|
+
// Executor stays unaware of AgentLoop or stream-json mode via the small
|
|
39
|
+
// ApprovalNotifier seam. Tests that stub the executor may omit the
|
|
40
|
+
// setter; skip silently in that case.
|
|
41
|
+
if (typeof this.executor.setApprovalNotifier === "function") {
|
|
42
|
+
this.executor.setApprovalNotifier(this);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* ApprovalNotifier implementation: emits structured stream events around
|
|
47
|
+
* every approval gate so consumers can correlate the request with the
|
|
48
|
+
* matching tool.call / tool.result lifecycle.
|
|
49
|
+
*/
|
|
50
|
+
notifyApprovalRequest(request) {
|
|
51
|
+
this.emit({
|
|
52
|
+
type: "approval.request",
|
|
53
|
+
id: request.toolCallId,
|
|
54
|
+
tool: request.toolName,
|
|
55
|
+
input: request.input,
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
notifyApprovalResolved(event) {
|
|
60
|
+
this.emit({
|
|
61
|
+
type: "approval.resolved",
|
|
62
|
+
id: event.toolCallId,
|
|
63
|
+
approved: event.approved,
|
|
64
|
+
reason: event.reason,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
});
|
|
37
67
|
}
|
|
38
68
|
async run(session, userMessage, attachments = [], signal) {
|
|
39
69
|
try {
|
|
@@ -66,10 +96,17 @@ export class AgentLoop {
|
|
|
66
96
|
})),
|
|
67
97
|
});
|
|
68
98
|
await this.saveSession(session);
|
|
69
|
-
const systemPrompt = this.contextBuilder.buildSystemPrompt();
|
|
70
99
|
const tools = this.registry.toSchemas();
|
|
100
|
+
// Build the base system prompt once per run. It loads AGENTS.md from disk,
|
|
101
|
+
// so rebuilding it each turn would let a tool that writes AGENTS.md inject
|
|
102
|
+
// freshly generated text as system instructions mid-run. Only the runtime
|
|
103
|
+
// context is refreshed per turn.
|
|
104
|
+
const baseSystemPrompt = this.contextBuilder.buildSystemPrompt();
|
|
71
105
|
let finalStopReason = "max_iterations";
|
|
72
106
|
let status = "max_iterations";
|
|
107
|
+
// Track the last logged system prompt so the model_request event is only
|
|
108
|
+
// written when the assembled prompt (including runtime context) changes.
|
|
109
|
+
let lastLoggedSystemPrompt;
|
|
73
110
|
this.emit({
|
|
74
111
|
type: "run.started",
|
|
75
112
|
sessionId: session.id,
|
|
@@ -83,12 +120,19 @@ export class AgentLoop {
|
|
|
83
120
|
return;
|
|
84
121
|
}
|
|
85
122
|
const modelContextItems = await this.buildModelContextItems(session);
|
|
86
|
-
const
|
|
123
|
+
const systemPrompt = await this.contextBuilder.appendRuntimeContext(baseSystemPrompt);
|
|
124
|
+
if (systemPrompt !== lastLoggedSystemPrompt) {
|
|
125
|
+
await this.eventStore.append(session.id, "model_request", {
|
|
126
|
+
systemPrompt,
|
|
127
|
+
messageCount: modelContextItems.length,
|
|
128
|
+
});
|
|
129
|
+
lastLoggedSystemPrompt = systemPrompt;
|
|
130
|
+
}
|
|
87
131
|
const response = await this.getModelResponse({
|
|
88
132
|
systemPrompt,
|
|
89
133
|
sessionId: session.id,
|
|
90
|
-
conversationItems,
|
|
91
|
-
messages: conversationItemsToMessages(
|
|
134
|
+
conversationItems: modelContextItems,
|
|
135
|
+
messages: conversationItemsToMessages(modelContextItems),
|
|
92
136
|
tools,
|
|
93
137
|
signal,
|
|
94
138
|
});
|
|
@@ -526,6 +570,24 @@ export class AgentLoop {
|
|
|
526
570
|
console.log(event.isError ? chalk.red(` ✗ ${preview}`) : chalk.gray(` ${preview}`));
|
|
527
571
|
return;
|
|
528
572
|
}
|
|
573
|
+
case "approval.request": {
|
|
574
|
+
// The readline responder in `askApprovalOn` prints the actual prompt;
|
|
575
|
+
// emitting one here would show it twice in human mode. The structured
|
|
576
|
+
// `approval.resolved` event still prints the audit decision below so
|
|
577
|
+
// the user sees the outcome of every gate.
|
|
578
|
+
this.finishPendingTerminalDelta();
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
case "approval.resolved": {
|
|
582
|
+
this.finishPendingTerminalDelta();
|
|
583
|
+
if (event.approved) {
|
|
584
|
+
console.log(chalk.gray(" approved"));
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
console.log(chalk.red(` denied (${event.reason})`));
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
529
591
|
}
|
|
530
592
|
}
|
|
531
593
|
finishPendingTerminalDelta() {
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
1
2
|
import { Command } from "commander";
|
|
3
|
+
import type { ApprovalAnswer, ApprovalRequest } from "../types/approval.js";
|
|
2
4
|
import { type ModelOption, type ModelOptionsResult } from "../models/resolve.js";
|
|
3
5
|
export declare function formatModelOptions(options?: readonly ModelOption[]): string;
|
|
4
6
|
export declare function formatModelOptionsJson(result: ModelOptionsResult): string;
|
|
7
|
+
/**
|
|
8
|
+
* Builds an approval responder suited to the current CLI mode.
|
|
9
|
+
*
|
|
10
|
+
* Selection rules (matches the issue #75 spec):
|
|
11
|
+
* - `--auto-approve` (with or without `--stream-json`): never prompts the
|
|
12
|
+
* user. Always resolves true so the Executor emits the audit events but
|
|
13
|
+
* the user is never interrupted.
|
|
14
|
+
* - `--stream-json` with a piped stdin (no TTY): the stdin line protocol
|
|
15
|
+
* responder. It reads JSON lines like
|
|
16
|
+
* `{"type":"approval.response","id":"<toolCallId>","approved":<bool>}`
|
|
17
|
+
* from stdin and never writes a prompt to stdout/stderr.
|
|
18
|
+
* - Otherwise (no flags or `--stream-json` with a TTY stdin): the readline
|
|
19
|
+
* prompt, identical to today's behavior.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createApprovalResponder(options?: {
|
|
22
|
+
autoApprove?: boolean;
|
|
23
|
+
streamJson?: boolean;
|
|
24
|
+
}): (request: ApprovalRequest, signal?: AbortSignal) => Promise<ApprovalAnswer>;
|
|
25
|
+
export declare function askApprovalOn(streams: {
|
|
26
|
+
input: NodeJS.ReadableStream;
|
|
27
|
+
output: NodeJS.WritableStream;
|
|
28
|
+
}, request: ApprovalRequest, signal?: AbortSignal, hooks?: {
|
|
29
|
+
onReadline?: (rl: readline.Interface) => void;
|
|
30
|
+
}): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Stdin line protocol responder used when `--stream-json` is active and
|
|
33
|
+
* stdin is not a TTY. Reads JSON lines from stdin and resolves the pending
|
|
34
|
+
* approval when a matching response arrives. See issue #75 §4 for the
|
|
35
|
+
* protocol and the mismatch / malformed / EOF / abort cases.
|
|
36
|
+
*
|
|
37
|
+
* Invariant: at most one approval is pending at a time (Executor runs tool
|
|
38
|
+
* calls sequentially). The responder therefore buffers exactly one pending
|
|
39
|
+
* request and discards responses received while no request is in flight.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createStdinApprovalResponder(streams: {
|
|
42
|
+
input: NodeJS.ReadableStream;
|
|
43
|
+
stderr?: NodeJS.WritableStream;
|
|
44
|
+
}): ((request: ApprovalRequest, signal?: AbortSignal) => Promise<ApprovalAnswer>) & {
|
|
45
|
+
/**
|
|
46
|
+
* Detach the readline interface from stdin. The CLI calls this in a
|
|
47
|
+
* `finally` after the run completes so a long-lived orchestrator piping
|
|
48
|
+
* JSON events on stdout and approval responses on stdin does not hang.
|
|
49
|
+
* Idempotent and safe to call multiple times.
|
|
50
|
+
*/
|
|
51
|
+
close(): void;
|
|
52
|
+
};
|
|
5
53
|
export declare function createCLI(): Command;
|
package/dist/cli/index.js
CHANGED
|
@@ -90,17 +90,170 @@ async function createAgentsFile(filePath, force) {
|
|
|
90
90
|
function isAlreadyExistsError(err) {
|
|
91
91
|
return err instanceof Error && "code" in err && err.code === "EEXIST";
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
function askApproval(request, signal) {
|
|
94
|
+
return askApprovalOn({ input: process.stdin, output: process.stdout }, request, signal);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Builds an approval responder suited to the current CLI mode.
|
|
98
|
+
*
|
|
99
|
+
* Selection rules (matches the issue #75 spec):
|
|
100
|
+
* - `--auto-approve` (with or without `--stream-json`): never prompts the
|
|
101
|
+
* user. Always resolves true so the Executor emits the audit events but
|
|
102
|
+
* the user is never interrupted.
|
|
103
|
+
* - `--stream-json` with a piped stdin (no TTY): the stdin line protocol
|
|
104
|
+
* responder. It reads JSON lines like
|
|
105
|
+
* `{"type":"approval.response","id":"<toolCallId>","approved":<bool>}`
|
|
106
|
+
* from stdin and never writes a prompt to stdout/stderr.
|
|
107
|
+
* - Otherwise (no flags or `--stream-json` with a TTY stdin): the readline
|
|
108
|
+
* prompt, identical to today's behavior.
|
|
109
|
+
*/
|
|
110
|
+
export function createApprovalResponder(options = {}) {
|
|
111
|
+
if (options.autoApprove) {
|
|
112
|
+
return async () => true;
|
|
113
|
+
}
|
|
114
|
+
if (options.streamJson && !process.stdin.isTTY) {
|
|
115
|
+
return createStdinApprovalResponder({
|
|
116
|
+
input: process.stdin,
|
|
117
|
+
stderr: process.stderr,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return askApproval;
|
|
121
|
+
}
|
|
122
|
+
export async function askApprovalOn(streams, request, signal, hooks) {
|
|
94
123
|
const rl = readline.createInterface({
|
|
95
|
-
input:
|
|
96
|
-
output:
|
|
124
|
+
input: streams.input,
|
|
125
|
+
output: streams.output,
|
|
97
126
|
});
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
127
|
+
hooks?.onReadline?.(rl);
|
|
128
|
+
const summary = request.toolName === "run_command"
|
|
129
|
+
? request.input.command ?? JSON.stringify(request.input)
|
|
130
|
+
: JSON.stringify(request.input);
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
let settled = false;
|
|
133
|
+
const settle = (value) => {
|
|
134
|
+
if (settled)
|
|
135
|
+
return;
|
|
136
|
+
settled = true;
|
|
137
|
+
signal?.removeEventListener("abort", onAbort);
|
|
138
|
+
rl.removeListener("SIGINT", onRlSigint);
|
|
139
|
+
rl.removeListener("close", onRlClose);
|
|
140
|
+
rl.close();
|
|
141
|
+
resolve(value);
|
|
142
|
+
};
|
|
143
|
+
const onAbort = () => settle(false);
|
|
144
|
+
// Readline captures Ctrl-C on a TTY and pauses stdin instead of letting
|
|
145
|
+
// the process-level SIGINT handler fire. Listen for the interface's own
|
|
146
|
+
// SIGINT event so a first Ctrl-C still cancels the prompt.
|
|
147
|
+
const onRlSigint = () => settle(false);
|
|
148
|
+
const onRlClose = () => settle(false);
|
|
149
|
+
if (signal?.aborted) {
|
|
150
|
+
settle(false);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
154
|
+
rl.on("SIGINT", onRlSigint);
|
|
155
|
+
rl.on("close", onRlClose);
|
|
156
|
+
rl.question(chalk.yellow(`Allow ${request.toolName}: ${summary}? [y/n] `), (answer) => settle(answer.toLowerCase().startsWith("y")));
|
|
101
157
|
});
|
|
102
|
-
|
|
103
|
-
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Stdin line protocol responder used when `--stream-json` is active and
|
|
161
|
+
* stdin is not a TTY. Reads JSON lines from stdin and resolves the pending
|
|
162
|
+
* approval when a matching response arrives. See issue #75 §4 for the
|
|
163
|
+
* protocol and the mismatch / malformed / EOF / abort cases.
|
|
164
|
+
*
|
|
165
|
+
* Invariant: at most one approval is pending at a time (Executor runs tool
|
|
166
|
+
* calls sequentially). The responder therefore buffers exactly one pending
|
|
167
|
+
* request and discards responses received while no request is in flight.
|
|
168
|
+
*/
|
|
169
|
+
export function createStdinApprovalResponder(streams) {
|
|
170
|
+
let pending;
|
|
171
|
+
let closed = false;
|
|
172
|
+
const onLine = (line) => {
|
|
173
|
+
const trimmed = line.trim();
|
|
174
|
+
if (!trimmed)
|
|
175
|
+
return;
|
|
176
|
+
let parsed;
|
|
177
|
+
try {
|
|
178
|
+
parsed = JSON.parse(trimmed);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
// Malformed JSON: log to stderr, discard, do not resolve.
|
|
182
|
+
streams.stderr?.write(`anita: discarded malformed approval.response line (${err.message})\n`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!parsed ||
|
|
186
|
+
typeof parsed !== "object" ||
|
|
187
|
+
parsed.type !== "approval.response") {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const { id, approved } = parsed;
|
|
191
|
+
if (typeof id !== "string" || typeof approved !== "boolean") {
|
|
192
|
+
streams.stderr?.write("anita: discarded approval.response with invalid id/approved\n");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!pending) {
|
|
196
|
+
streams.stderr?.write("anita: discarded approval.response with no pending request\n");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (pending.request.toolCallId !== id) {
|
|
200
|
+
// Desync recovery: don't poison the run with a stale response.
|
|
201
|
+
streams.stderr?.write(`anita: discarded approval.response for unknown id ${id} (waiting on ${pending.request.toolCallId})\n`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const current = pending;
|
|
205
|
+
pending = undefined;
|
|
206
|
+
current.resolve(approved);
|
|
207
|
+
};
|
|
208
|
+
const onEnd = () => {
|
|
209
|
+
closed = true;
|
|
210
|
+
if (pending) {
|
|
211
|
+
const current = pending;
|
|
212
|
+
pending = undefined;
|
|
213
|
+
// EOF while a request is in flight must surface as `reason: "eof"` so
|
|
214
|
+
// consumers can distinguish "user said no" from "consumer closed the
|
|
215
|
+
// pipe mid-run" (issue #75 §4).
|
|
216
|
+
current.resolve({ approved: false, reason: "eof" });
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const rl = readline.createInterface({ input: streams.input, crlfDelay: Infinity });
|
|
220
|
+
rl.on("line", onLine);
|
|
221
|
+
rl.on("close", onEnd);
|
|
222
|
+
// Returns a function that disposes the stdin reader. The chat command calls
|
|
223
|
+
// this from a `finally` block so a long-lived orchestrator piping JSON
|
|
224
|
+
// events on stdout and approval responses on stdin does not hang after
|
|
225
|
+
// `loop.run` returns. The responder is single-use: after close, further
|
|
226
|
+
// calls short-circuit to a structured EOF answer.
|
|
227
|
+
function close() {
|
|
228
|
+
if (closed)
|
|
229
|
+
return;
|
|
230
|
+
closed = true;
|
|
231
|
+
rl.close();
|
|
232
|
+
if (pending) {
|
|
233
|
+
const current = pending;
|
|
234
|
+
pending = undefined;
|
|
235
|
+
current.resolve({ approved: false, reason: "eof" });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const answer = async (request, signal) => {
|
|
239
|
+
if (closed)
|
|
240
|
+
return { approved: false, reason: "eof" };
|
|
241
|
+
return new Promise((resolve) => {
|
|
242
|
+
pending = { request, resolve };
|
|
243
|
+
const settle = (value) => {
|
|
244
|
+
if (pending && pending.request.toolCallId === request.toolCallId) {
|
|
245
|
+
pending = undefined;
|
|
246
|
+
resolve(value);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
if (signal?.aborted) {
|
|
250
|
+
settle({ approved: false, reason: "eof" });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
signal?.addEventListener("abort", () => settle(false), { once: true });
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
return Object.assign(answer, { close });
|
|
104
257
|
}
|
|
105
258
|
export function createCLI() {
|
|
106
259
|
const program = new Command();
|
|
@@ -217,7 +370,10 @@ export function createCLI() {
|
|
|
217
370
|
skills,
|
|
218
371
|
systemPrompt,
|
|
219
372
|
});
|
|
220
|
-
const approvalFn =
|
|
373
|
+
const approvalFn = createApprovalResponder({
|
|
374
|
+
autoApprove,
|
|
375
|
+
streamJson,
|
|
376
|
+
});
|
|
221
377
|
const executor = new Executor(registry, policyEngine, eventStore, approvalFn);
|
|
222
378
|
const loop = new AgentLoop(provider, executor, contextBuilder, registry, eventStore, sessionStore, streamJson);
|
|
223
379
|
if (!streamJson) {
|
|
@@ -262,6 +418,14 @@ export function createCLI() {
|
|
|
262
418
|
}
|
|
263
419
|
finally {
|
|
264
420
|
process.off("SIGINT", onSigint);
|
|
421
|
+
// Detach any stdin readline the responder opened so the process can
|
|
422
|
+
// exit naturally instead of waiting for stdin to close. The
|
|
423
|
+
// auto-approve and readline responders return plain async functions
|
|
424
|
+
// without a `close` method; only the stdin line-protocol responder
|
|
425
|
+
// exposes one.
|
|
426
|
+
const close = approvalFn.close;
|
|
427
|
+
if (typeof close === "function")
|
|
428
|
+
close();
|
|
265
429
|
}
|
|
266
430
|
if (interrupted) {
|
|
267
431
|
process.exit(130);
|
package/dist/models/resolve.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModelProvider } from "./provider.js";
|
|
2
|
-
export declare const OLLAMA_CLOUD_MODELS:
|
|
2
|
+
export declare const OLLAMA_CLOUD_MODELS: string[];
|
|
3
3
|
export type ModelOptionGroupName = "Ollama Local" | "Ollama Cloud" | "OpenRouter";
|
|
4
4
|
export interface ModelOption {
|
|
5
5
|
label: string;
|
package/dist/models/resolve.js
CHANGED
|
@@ -12,64 +12,72 @@ const DEEPSEEK_V4_PRO_CONTEXT_WINDOW_TOKENS = 1_000_000;
|
|
|
12
12
|
const KIMI_K2_CONTEXT_WINDOW_TOKENS = 256_000;
|
|
13
13
|
const MINIMAX_M3_OLLAMA_CONTEXT_WINDOW_TOKENS = 512_000;
|
|
14
14
|
const MINIMAX_M3_OPENROUTER_CONTEXT_WINDOW_TOKENS = 1_000_000;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const OLLAMA_CLOUD_MODEL_SPECS = [
|
|
16
|
+
{
|
|
17
|
+
slug: "glm-5.2",
|
|
18
|
+
label: "GLM 5.2",
|
|
19
|
+
contextWindowTokens: GLM_5_2_CONTEXT_WINDOW_TOKENS,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
slug: "minimax-m3",
|
|
23
|
+
label: "MiniMax M3",
|
|
24
|
+
contextWindowTokens: MINIMAX_M3_OLLAMA_CONTEXT_WINDOW_TOKENS,
|
|
25
|
+
capabilities: { attachments: { images: true, files: true } },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
slug: "deepseek-v4-pro",
|
|
29
|
+
label: "DeepSeek V4 Pro",
|
|
30
|
+
contextWindowTokens: DEEPSEEK_V4_PRO_CONTEXT_WINDOW_TOKENS,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
slug: "kimi-k2.7-code",
|
|
34
|
+
label: "Kimi K2.7 Code",
|
|
35
|
+
contextWindowTokens: KIMI_K2_CONTEXT_WINDOW_TOKENS,
|
|
36
|
+
capabilities: { attachments: { images: true, files: false } },
|
|
37
|
+
},
|
|
20
38
|
];
|
|
21
|
-
const
|
|
22
|
-
"glm-5.2": GLM_5_2_CONTEXT_WINDOW_TOKENS,
|
|
23
|
-
"minimax-m3": MINIMAX_M3_OLLAMA_CONTEXT_WINDOW_TOKENS,
|
|
24
|
-
"deepseek-v4-pro": DEEPSEEK_V4_PRO_CONTEXT_WINDOW_TOKENS,
|
|
25
|
-
"kimi-k2.7-code": KIMI_K2_CONTEXT_WINDOW_TOKENS,
|
|
26
|
-
};
|
|
39
|
+
export const OLLAMA_CLOUD_MODELS = OLLAMA_CLOUD_MODEL_SPECS.map((spec) => spec.slug);
|
|
27
40
|
export const MODEL_OPTIONS = [
|
|
28
41
|
{
|
|
29
|
-
label: "GLM 4.7 Flash
|
|
42
|
+
label: "GLM 4.7 Flash",
|
|
30
43
|
value: "ollama/glm-4.7-flash:latest",
|
|
31
44
|
group: "Ollama Local",
|
|
32
45
|
contextWindowTokens: GLM_CONTEXT_WINDOW_TOKENS,
|
|
33
46
|
},
|
|
34
47
|
{
|
|
35
|
-
label: "GLM 5.1
|
|
48
|
+
label: "GLM 5.1",
|
|
36
49
|
value: "openrouter/z-ai/glm-5.1",
|
|
37
50
|
group: "OpenRouter",
|
|
38
51
|
contextWindowTokens: GLM_CONTEXT_WINDOW_TOKENS,
|
|
39
52
|
capabilities: { attachments: { images: false, files: true } },
|
|
40
53
|
},
|
|
41
54
|
{
|
|
42
|
-
label: "MiniMax M3
|
|
55
|
+
label: "MiniMax M3",
|
|
43
56
|
value: "openrouter/minimax/minimax-m3",
|
|
44
57
|
group: "OpenRouter",
|
|
45
58
|
contextWindowTokens: MINIMAX_M3_OPENROUTER_CONTEXT_WINDOW_TOKENS,
|
|
46
59
|
capabilities: { attachments: { images: true, files: true } },
|
|
47
60
|
},
|
|
48
61
|
{
|
|
49
|
-
label: "DeepSeek V4 Pro
|
|
62
|
+
label: "DeepSeek V4 Pro",
|
|
50
63
|
value: "openrouter/deepseek/deepseek-v4-pro",
|
|
51
64
|
group: "OpenRouter",
|
|
52
65
|
contextWindowTokens: DEEPSEEK_V4_PRO_CONTEXT_WINDOW_TOKENS,
|
|
53
66
|
capabilities: { attachments: { images: false, files: true } },
|
|
54
67
|
},
|
|
55
68
|
{
|
|
56
|
-
label: "Kimi K2.6
|
|
69
|
+
label: "Kimi K2.6",
|
|
57
70
|
value: "openrouter/moonshotai/kimi-k2.6",
|
|
58
71
|
group: "OpenRouter",
|
|
59
72
|
contextWindowTokens: KIMI_K2_CONTEXT_WINDOW_TOKENS,
|
|
60
73
|
capabilities: { attachments: { images: true, files: true } },
|
|
61
74
|
},
|
|
62
|
-
...
|
|
63
|
-
label:
|
|
64
|
-
value: `ollama-cloud/${
|
|
75
|
+
...OLLAMA_CLOUD_MODEL_SPECS.map((spec) => ({
|
|
76
|
+
label: spec.label,
|
|
77
|
+
value: `ollama-cloud/${spec.slug}`,
|
|
65
78
|
group: "Ollama Cloud",
|
|
66
|
-
contextWindowTokens:
|
|
67
|
-
...(
|
|
68
|
-
? { capabilities: { attachments: { images: true, files: true } } }
|
|
69
|
-
: {}),
|
|
70
|
-
...(model === "kimi-k2.7-code"
|
|
71
|
-
? { capabilities: { attachments: { images: true, files: false } } }
|
|
72
|
-
: {}),
|
|
79
|
+
contextWindowTokens: spec.contextWindowTokens,
|
|
80
|
+
...(spec.capabilities ? { capabilities: spec.capabilities } : {}),
|
|
73
81
|
})),
|
|
74
82
|
];
|
|
75
83
|
export function parseModelString(modelString) {
|
|
@@ -112,7 +120,7 @@ export async function discoverLocalOllamaModelOptions(fetchImpl = fetch) {
|
|
|
112
120
|
return undefined;
|
|
113
121
|
const payload = await response.json();
|
|
114
122
|
return parseOllamaModelNames(payload).map((name) => ({
|
|
115
|
-
label:
|
|
123
|
+
label: name,
|
|
116
124
|
value: `ollama/${name}`,
|
|
117
125
|
group: "Ollama Local",
|
|
118
126
|
contextWindowTokens: UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the approval seam between Executor and consumers.
|
|
3
|
+
*
|
|
4
|
+
* The `Executor` exposes a single approval callback. To make stream-json +
|
|
5
|
+
* approvals work, the callback now receives a structured request (with the
|
|
6
|
+
* model's tool call id) and a small notifier seam emits structured events
|
|
7
|
+
* around every approval gate so consumers can audit decisions.
|
|
8
|
+
*/
|
|
9
|
+
export interface ApprovalRequest {
|
|
10
|
+
/** The model's tool call id; matches `tool.call.id` / `tool.result.id`. */
|
|
11
|
+
toolCallId: string;
|
|
12
|
+
toolName: string;
|
|
13
|
+
input: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reason a pending approval was settled. Echoed in `approval.resolved`.
|
|
17
|
+
*
|
|
18
|
+
* - `user`: the user/consumer answered the prompt.
|
|
19
|
+
* - `aborted`: the run's AbortSignal fired while waiting for the answer.
|
|
20
|
+
* - `eof`: the consumer closed stdin before responding (stdin line protocol).
|
|
21
|
+
* - `error`: the approval callback itself threw.
|
|
22
|
+
*/
|
|
23
|
+
export type ApprovalResolvedReason = "user" | "aborted" | "eof" | "error";
|
|
24
|
+
/**
|
|
25
|
+
* What a resolver hands back to the executor. A plain boolean still works
|
|
26
|
+
* (treated as `{ approved: <value>, reason: "user" }`) for callers that
|
|
27
|
+
* don't care about distinguishing EOF/aborted; responders that need to
|
|
28
|
+
* signal "stdin closed mid-run" return `{ approved: false, reason: "eof" }`.
|
|
29
|
+
*/
|
|
30
|
+
export type ApprovalAnswer = boolean | {
|
|
31
|
+
approved: boolean;
|
|
32
|
+
reason?: "user" | "eof";
|
|
33
|
+
};
|
|
34
|
+
export interface ApprovalResolvedEvent {
|
|
35
|
+
toolCallId: string;
|
|
36
|
+
toolName: string;
|
|
37
|
+
approved: boolean;
|
|
38
|
+
reason: ApprovalResolvedReason;
|
|
39
|
+
/** Optional human-readable detail for the `error` reason. */
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolves the pending approval request. Implementations live in the CLI
|
|
44
|
+
* (readline, stdin line protocol, or always-true under --auto-approve).
|
|
45
|
+
*/
|
|
46
|
+
export type ApprovalCallback = (request: ApprovalRequest, signal?: AbortSignal) => Promise<ApprovalAnswer>;
|
|
47
|
+
/**
|
|
48
|
+
* Small injected seam so `Executor` can emit stream-json approval events
|
|
49
|
+
* without depending on `AgentLoop`. The CLI's `AgentLoop` implements this by
|
|
50
|
+
* calling its own `emit(...)` with `approval.request` / `approval.resolved`.
|
|
51
|
+
*/
|
|
52
|
+
export interface ApprovalNotifier {
|
|
53
|
+
notifyApprovalRequest(request: ApprovalRequest): void;
|
|
54
|
+
notifyApprovalResolved(event: ApprovalResolvedEvent): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the approval seam between Executor and consumers.
|
|
3
|
+
*
|
|
4
|
+
* The `Executor` exposes a single approval callback. To make stream-json +
|
|
5
|
+
* approvals work, the callback now receives a structured request (with the
|
|
6
|
+
* model's tool call id) and a small notifier seam emits structured events
|
|
7
|
+
* around every approval gate so consumers can audit decisions.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/types/events.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type EventType = "session_start" | "user_message" | "assistant_reasoning" | "assistant_response" | "conversation_compaction" | "tool_call" | "tool_result" | "policy_decision" | "error" | "run_cancelled" | "session_end" | "session_archived" | "skills_loaded";
|
|
1
|
+
export type EventType = "session_start" | "user_message" | "model_request" | "assistant_reasoning" | "assistant_response" | "conversation_compaction" | "tool_call" | "tool_result" | "policy_decision" | "error" | "run_cancelled" | "session_end" | "session_archived" | "skills_loaded";
|
|
2
2
|
export interface AgentEvent {
|
|
3
3
|
id: string;
|
|
4
4
|
sessionId: string;
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -36,6 +36,18 @@ export type StreamEvent = {
|
|
|
36
36
|
content: string;
|
|
37
37
|
isError: boolean;
|
|
38
38
|
metadata?: Record<string, ToolResultMetadata>;
|
|
39
|
+
} | {
|
|
40
|
+
type: "approval.request";
|
|
41
|
+
id: string;
|
|
42
|
+
tool: string;
|
|
43
|
+
input: Record<string, unknown>;
|
|
44
|
+
timestamp: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: "approval.resolved";
|
|
47
|
+
id: string;
|
|
48
|
+
approved: boolean;
|
|
49
|
+
reason: "user" | "aborted" | "eof" | "error";
|
|
50
|
+
timestamp: string;
|
|
39
51
|
} | {
|
|
40
52
|
type: "run.completed";
|
|
41
53
|
sessionId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@germanescobar/anita",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "An intelligent AI-powered coding agent that helps you write, edit, and run code with conversational access to AI models and your file system.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|