loopengine 0.0.18 → 0.0.21
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/PROTOCOL.md +245 -0
- package/README.md +44 -5
- package/core/system-skills/composio-large-outputs/SKILL.md +25 -0
- package/dist/{cli.d.ts → bin/cli.d.ts} +25 -4
- package/dist/{cli.js → bin/cli.js} +131 -16
- package/dist/{agent-as-tool.d.ts → core/agent-as-tool.d.ts} +2 -2
- package/dist/{agent-as-tool.js → core/agent-as-tool.js} +1 -1
- package/dist/core/client.d.ts +113 -0
- package/dist/core/client.js +123 -0
- package/dist/core/discover-agents.d.ts +45 -0
- package/dist/{discover-agents.js → core/discover-agents.js} +11 -3
- package/dist/{gateway-tools.d.ts → core/gateway-tools.d.ts} +38 -6
- package/dist/{gateway-tools.js → core/gateway-tools.js} +140 -28
- package/dist/core/loop-events.d.ts +175 -0
- package/dist/core/loop-events.js +1 -0
- package/dist/{model-calls → core/model-calls}/anthropic-model-call.d.ts +1 -1
- package/dist/{model-calls → core/model-calls}/deepseek-model-call.d.ts +1 -1
- package/dist/{model-calls → core/model-calls}/openai-model-call.d.ts +1 -1
- package/dist/{model-calls → core/model-calls}/openai-model-call.js +1 -1
- package/dist/{run-agent.d.ts → core/run-agent.d.ts} +79 -11
- package/dist/{run-agent.js → core/run-agent.js} +242 -60
- package/dist/{session-store.d.ts → core/session-store.d.ts} +14 -2
- package/dist/{session-store.js → core/session-store.js} +19 -0
- package/dist/core/system-tools/ask_user.d.ts +72 -0
- package/dist/core/system-tools/ask_user.js +136 -0
- package/dist/core/system-tools/index.d.ts +3 -0
- package/dist/core/system-tools/index.js +12 -0
- package/dist/core/system-tools/read_file.d.ts +28 -0
- package/dist/core/system-tools/read_file.js +64 -0
- package/dist/{vector-index.js → core/vector-index.js} +1 -1
- package/dist/index.d.ts +19 -16
- package/dist/index.js +16 -9
- package/package.json +34 -28
- package/protocol/loop-event.schema.json +257 -0
- package/dist/discover-agents.d.ts +0 -24
- /package/dist/{agent-config.d.ts → core/agent-config.d.ts} +0 -0
- /package/dist/{agent-config.js → core/agent-config.js} +0 -0
- /package/dist/{model-calls → core/model-calls}/anthropic-model-call.js +0 -0
- /package/dist/{model-calls → core/model-calls}/deepseek-model-call.js +0 -0
- /package/dist/{vector-index.d.ts → core/vector-index.d.ts} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { CheckResult } from './budget.js';
|
|
2
|
+
import type { Decision, PendingApproval } from 'actauth';
|
|
3
|
+
import type { PendingQuestion } from './system-tools/index.js';
|
|
4
|
+
/** BudgetTracker's own read-only budget check, run once per loop turn
|
|
5
|
+
* before the model call — reported unchanged (spread, not renamed) so
|
|
6
|
+
* this never drifts from whatever budget.ts's own CheckResult actually
|
|
7
|
+
* contains. */
|
|
8
|
+
export type BudgetCheckEvent = {
|
|
9
|
+
type: 'budget:check';
|
|
10
|
+
} & CheckResult;
|
|
11
|
+
/** compaction.ts's own recovery kicked in because the prompt was rejected as
|
|
12
|
+
* too large — `from`/`to` are message-array lengths (this turn's own
|
|
13
|
+
* newMessages included), not token counts; see run-agent.ts's own
|
|
14
|
+
* recovery.call onPromptTooLong hook. Named after *what triggered it and
|
|
15
|
+
* what happened* (the prompt was too long, so it got compacted), not
|
|
16
|
+
* after which module did the work — "compaction:recover" still didn't
|
|
17
|
+
* say this only ever fires for the prompt-too-long case specifically,
|
|
18
|
+
* same problem contextclip:check's own rename from a package name was
|
|
19
|
+
* fixing in the first place.
|
|
20
|
+
*
|
|
21
|
+
* No sibling event for recovery.ts's other two failure modes
|
|
22
|
+
* (media-too-large, truncated output) — neither has an onMediaTooLarge/
|
|
23
|
+
* onTruncated hook actually wired up in run-agent.ts today, so neither
|
|
24
|
+
* can currently fire at all. A prior 'recovery:summary' aggregate event
|
|
25
|
+
* covering all three was removed for being ~100% redundant with this one
|
|
26
|
+
* while that's true; it can come back once a second real recovery type
|
|
27
|
+
* exists to aggregate. */
|
|
28
|
+
export interface PromptCompactionEvent {
|
|
29
|
+
type: 'prompt:compaction';
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
}
|
|
33
|
+
/** The model's own "I'll do X" preamble alongside a tool_use request —
|
|
34
|
+
* the only *live* signal of assistant text mid-turn (the final answer
|
|
35
|
+
* only ever arrives via 'loop:done'/'done', once, at the very end). */
|
|
36
|
+
export interface AssistantTextEvent {
|
|
37
|
+
type: 'assistant:text';
|
|
38
|
+
text: string;
|
|
39
|
+
}
|
|
40
|
+
/** One ActAuth Gate.evaluate() outcome per requested tool call — 'allow'
|
|
41
|
+
* here can still mean a human was asked interactively and said yes (see
|
|
42
|
+
* run-agent.ts's own wasAskedInteractively); this event fires regardless,
|
|
43
|
+
* the interactive case just also gets its own approval:pending. */
|
|
44
|
+
export interface ActauthDecisionEvent {
|
|
45
|
+
type: 'actauth:decision';
|
|
46
|
+
tool: string;
|
|
47
|
+
decision: Decision;
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
/** A tool call's decision is in and it's about to run (or, for a system
|
|
51
|
+
* tool, already bypassed the gate entirely) — fired the instant that's
|
|
52
|
+
* known, not once execution finishes, so a live caller can show
|
|
53
|
+
* "Running…" immediately instead of a silent gap. Matched to its later
|
|
54
|
+
* 'tool:result' by `id` (the model's own tool_use id). */
|
|
55
|
+
export interface ToolStartedEvent {
|
|
56
|
+
type: 'tool:started';
|
|
57
|
+
id: string;
|
|
58
|
+
tool: string;
|
|
59
|
+
args: Record<string, unknown>;
|
|
60
|
+
detailText: string;
|
|
61
|
+
}
|
|
62
|
+
/** A tool call's resolution — auto-allowed, interactively approved,
|
|
63
|
+
* denied, or skipped alike. `args`/`detailText` are omitted for some
|
|
64
|
+
* skipped/denied paths that never had them resolved (see run-agent.ts's
|
|
65
|
+
* own call sites). */
|
|
66
|
+
export interface ToolResultEvent {
|
|
67
|
+
type: 'tool:result';
|
|
68
|
+
id: string;
|
|
69
|
+
tool: string;
|
|
70
|
+
args?: Record<string, unknown>;
|
|
71
|
+
detailText?: string;
|
|
72
|
+
statusText: string;
|
|
73
|
+
}
|
|
74
|
+
/** ToolLane's own raw fulfil/reject outcome, logged alongside (not
|
|
75
|
+
* instead of) the richer 'tool:result' above — kept as its own event
|
|
76
|
+
* since it's toollane's own vocabulary (`summary`, not `statusText`), not
|
|
77
|
+
* reshaped to match. */
|
|
78
|
+
export interface ToolLaneResultEvent {
|
|
79
|
+
type: 'toollane:result';
|
|
80
|
+
name: string;
|
|
81
|
+
summary: string;
|
|
82
|
+
}
|
|
83
|
+
/** A `Skill` tool_use block was resolved to a real skill body via
|
|
84
|
+
* SkillGarden's own lazy "index-now-load-later" design — not gated by
|
|
85
|
+
* ActAuth (see run-agent.ts's own comment on why Skill invocation isn't
|
|
86
|
+
* a real tool call). Named after what happened (the skill's full body
|
|
87
|
+
* got loaded), not after which library method was called — same
|
|
88
|
+
* reasoning budget:check's own rename from contextclip:check used. */
|
|
89
|
+
export interface SkillLoadEvent {
|
|
90
|
+
type: 'skill:loaded';
|
|
91
|
+
skill: string;
|
|
92
|
+
}
|
|
93
|
+
/** An approved call never ran because a sibling call in the same turn's
|
|
94
|
+
* batch was denied — the whole batch cancels together (see
|
|
95
|
+
* RunAgentResult.stopReason's own doc comment). */
|
|
96
|
+
export interface LoopSkippedEvent {
|
|
97
|
+
type: 'loop:skipped';
|
|
98
|
+
name: string;
|
|
99
|
+
deniedTools: string[];
|
|
100
|
+
}
|
|
101
|
+
/** Hit config.maxTurns without the model producing a final answer. */
|
|
102
|
+
export interface LoopMaxTurnsEvent {
|
|
103
|
+
type: 'loop:max_turns';
|
|
104
|
+
maxTurns: number;
|
|
105
|
+
}
|
|
106
|
+
/** A human denied at least one requested tool call this turn — the whole
|
|
107
|
+
* turn stops right there (see RunAgentResult.stopReason). */
|
|
108
|
+
export interface LoopDeniedEvent {
|
|
109
|
+
type: 'loop:denied';
|
|
110
|
+
deniedTools: string[];
|
|
111
|
+
}
|
|
112
|
+
/** The loop's own final answer — the model produced text with no more
|
|
113
|
+
* tool_use blocks. Distinct from the adapter-level 'done' below: this
|
|
114
|
+
* only ever means a genuine finish, never a synthetic max_turns/denied
|
|
115
|
+
* notice (see adapters/http.ts's own 'done', which covers all three). */
|
|
116
|
+
export interface LoopDoneEvent {
|
|
117
|
+
type: 'loop:done';
|
|
118
|
+
text: string;
|
|
119
|
+
}
|
|
120
|
+
/** Every event runAgent() itself can emit via RunAgentOptions.onEvent —
|
|
121
|
+
* not the adapter-synthesized ones below, which no caller of runAgent()
|
|
122
|
+
* itself ever produces. */
|
|
123
|
+
export type RunAgentLoopEvent = BudgetCheckEvent | PromptCompactionEvent | AssistantTextEvent | ActauthDecisionEvent | ToolStartedEvent | ToolResultEvent | ToolLaneResultEvent | SkillLoadEvent | LoopSkippedEvent | LoopMaxTurnsEvent | LoopDeniedEvent | LoopDoneEvent;
|
|
124
|
+
/** adapters/http.ts echoing back which session id this turn used — first
|
|
125
|
+
* event of every streamed turn, so a caller that omitted sessionId can
|
|
126
|
+
* capture what got generated. Not something runAgent() itself knows
|
|
127
|
+
* (session ids are resolved per-request by the adapter, before runAgent
|
|
128
|
+
* is even called). */
|
|
129
|
+
export interface SessionEvent {
|
|
130
|
+
type: 'session';
|
|
131
|
+
sessionId: string;
|
|
132
|
+
}
|
|
133
|
+
/** A tool call needs a human 'ask' decision, live — spreads actauth's own
|
|
134
|
+
* PendingApproval verbatim (id/tool/args/scope/reason/requestedAt)
|
|
135
|
+
* alongside `type`, so this never drifts from whatever that package
|
|
136
|
+
* actually returns. */
|
|
137
|
+
export type ApprovalPendingEvent = {
|
|
138
|
+
type: 'approval:pending';
|
|
139
|
+
} & PendingApproval;
|
|
140
|
+
/** The system ask_user tool raised a genuinely ambiguous question — same
|
|
141
|
+
* spread-verbatim reasoning as ApprovalPendingEvent above, against
|
|
142
|
+
* system-tools/ask_user.ts's own PendingQuestion. */
|
|
143
|
+
export type QuestionPendingEvent = {
|
|
144
|
+
type: 'question:pending';
|
|
145
|
+
} & PendingQuestion;
|
|
146
|
+
/** The turn is over — covers a genuine finish and both synthetic
|
|
147
|
+
* stopReasons (max_turns/denied) alike, the one event every caller can
|
|
148
|
+
* treat as "nothing more is coming" regardless of which of the three it
|
|
149
|
+
* was (see RunAgentResult.stopReason's own doc comment). */
|
|
150
|
+
export interface DoneEvent {
|
|
151
|
+
type: 'done';
|
|
152
|
+
text: string;
|
|
153
|
+
stopReason?: 'max_turns' | 'denied';
|
|
154
|
+
}
|
|
155
|
+
/** Something failed after the response was already committed (SSE
|
|
156
|
+
* headers sent, or a background turn already running) — surfaced in-band
|
|
157
|
+
* rather than as an HTTP status code, since it's too late for one. */
|
|
158
|
+
export interface ErrorEvent {
|
|
159
|
+
type: 'error';
|
|
160
|
+
error: string;
|
|
161
|
+
}
|
|
162
|
+
/** Synthesized by adapters/http.ts around a turn — session ids and
|
|
163
|
+
* HTTP-level errors aren't things runAgent() itself produces, but they're
|
|
164
|
+
* still part of the same lifecycle a caller observes end to end. */
|
|
165
|
+
export type AdapterLoopEvent = SessionEvent | ApprovalPendingEvent | QuestionPendingEvent | DoneEvent | ErrorEvent;
|
|
166
|
+
/** The full wire vocabulary — what run-agent.ts emits via onEvent, plus
|
|
167
|
+
* what adapters/http.ts (and, in --json mode, adapters/cli.ts) layers
|
|
168
|
+
* around it. This is "the standard output" every channel adapter is a
|
|
169
|
+
* projection of: adapters/http.ts's SSE stream writes one of these per
|
|
170
|
+
* frame; its plain /messages route collects the whole array into an
|
|
171
|
+
* `events` response field; adapters/cli.ts's --json mode writes one per
|
|
172
|
+
* NDJSON line; web/playground.ts is just a UI rendering whichever of
|
|
173
|
+
* those two HTTP routes it's talking to. */
|
|
174
|
+
export type LoopEvent = RunAgentLoopEvent | AdapterLoopEvent;
|
|
175
|
+
export type LoopEventType = LoopEvent['type'];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import type { ModelCall } from '#run-agent.js';
|
|
2
|
+
import type { ModelCall } from '#core/run-agent.js';
|
|
3
3
|
export interface AnthropicModelCallOptions {
|
|
4
4
|
/** Defaults to the ANTHROPIC_API_KEY env var, same as the SDK itself. */
|
|
5
5
|
apiKey?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import type { Message, ModelCall, ModelResponse } from '#run-agent.js';
|
|
2
|
+
import type { Message, ModelCall, ModelResponse } from '#core/run-agent.js';
|
|
3
3
|
type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
4
4
|
export interface OpenAIModelCallOptions {
|
|
5
5
|
/** Defaults to the OPENAI_API_KEY env var, same as the SDK itself. */
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// stop_reason is normalized to the vocabulary anthropic-model-call.ts
|
|
15
15
|
// already established ('end_turn' / 'tool_use' / 'max_tokens') rather than
|
|
16
|
-
// passed through as OpenAI's own finish_reason strings —
|
|
16
|
+
// passed through as OpenAI's own finish_reason strings — recovery.ts's
|
|
17
17
|
// defaultIsTruncated checks a translated ModelResponse for stop_reason ===
|
|
18
18
|
// 'max_tokens', so a provider-agnostic caller wiring up onTruncated later
|
|
19
19
|
// gets the same behavior regardless of which ModelCall is plugged in.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { RuleSet } from 'actauth';
|
|
2
|
-
import type { AgentConfig, ToolDefinition, ToolSchema } from '#agent-config.js';
|
|
1
|
+
import { RuleSet, type Approver } from 'actauth';
|
|
2
|
+
import type { AgentConfig, ToolDefinition, ToolSchema } from '#core/agent-config.js';
|
|
3
|
+
import { type PendingQuestion } from './system-tools/index.js';
|
|
4
|
+
import type { LoopEvent } from './loop-events.js';
|
|
5
|
+
export { systemTools } from './system-tools/index.js';
|
|
6
|
+
export type * from './loop-events.js';
|
|
7
|
+
export declare const systemSkillsDir: string;
|
|
8
|
+
export declare function dedupeToolsByName(list: ToolDefinition[]): ToolDefinition[];
|
|
3
9
|
export interface ModelContentBlock {
|
|
4
10
|
type: string;
|
|
5
11
|
/** text blocks. */
|
|
@@ -16,13 +22,20 @@ export interface ModelContentBlock {
|
|
|
16
22
|
content?: string;
|
|
17
23
|
/** tool_result blocks: true if the tool call failed or was denied. */
|
|
18
24
|
is_error?: boolean;
|
|
25
|
+
/** tool_result blocks: the actauth decision's own reason this call was
|
|
26
|
+
* allowed (e.g. "matched rule '...'"). Extra metadata riding alongside
|
|
27
|
+
* `content`, not folded into it — every model-calls/* adapter
|
|
28
|
+
* explicitly whitelists which fields it forwards to a real model API,
|
|
29
|
+
* so this never reaches the model, only a caller (the playground)
|
|
30
|
+
* inspecting stored history directly. */
|
|
31
|
+
reason?: string;
|
|
19
32
|
}
|
|
20
33
|
export interface ModelResponse {
|
|
21
34
|
stop_reason: string;
|
|
22
35
|
content: ModelContentBlock[];
|
|
23
36
|
}
|
|
24
37
|
/** loopengine's own conversation-message type — a superset of
|
|
25
|
-
*
|
|
38
|
+
* budget.ts's own `{role, content: string}`: `content` can also be a
|
|
26
39
|
* `ModelContentBlock[]`, the same block shape `ModelResponse.content`
|
|
27
40
|
* already used, so a model's tool_use requests and this loop's
|
|
28
41
|
* tool_result replies round-trip with real per-call identity
|
|
@@ -37,8 +50,10 @@ export interface Message {
|
|
|
37
50
|
/** Swap the Anthropic SDK in here — this is the only seam a real API call needs. */
|
|
38
51
|
export type ModelCall = (messages: Message[], system: string, tools: ToolSchema[]) => Promise<ModelResponse>;
|
|
39
52
|
export interface RunAgentOptions {
|
|
40
|
-
/** Emitted at each loop step for callers that want visibility (logging, UI, tests)
|
|
41
|
-
|
|
53
|
+
/** Emitted at each loop step for callers that want visibility (logging, UI, tests) —
|
|
54
|
+
* see loop-events.ts's own header comment for why this is a single typed
|
|
55
|
+
* union rather than a string name plus untyped detail. Default: no-op. */
|
|
56
|
+
onEvent?: (event: LoopEvent) => void;
|
|
42
57
|
/** The ActAuth tenant this call runs as — feeds the Gate's scope, so it
|
|
43
58
|
* governs which rules apply. runAgent() never sees a request, so it
|
|
44
59
|
* can't call AgentConfig.tenantFor itself; adapters/http.ts resolves it
|
|
@@ -46,6 +61,45 @@ export interface RunAgentOptions {
|
|
|
46
61
|
* omitting it — every standalone/CLI caller (which never has a request
|
|
47
62
|
* to resolve tenantFor from) gets that default automatically. */
|
|
48
63
|
tenant?: string;
|
|
64
|
+
/** This call's own session id (the *raw* one — see adapters/http.ts's
|
|
65
|
+
* own rawSessionId/storageSessionId distinction, this is the former),
|
|
66
|
+
* if the caller has one. runAgent() never sees a request or a
|
|
67
|
+
* SessionStore itself, so it can't derive this on its own — passed
|
|
68
|
+
* through purely so a pending ask_user question can be tagged with
|
|
69
|
+
* which session raised it (see system-tools/ask_user.ts's own
|
|
70
|
+
* PendingQuestion.sessionId), letting a caller list/answer questions
|
|
71
|
+
* scoped to one conversation instead of every one this process has ever
|
|
72
|
+
* seen. Omitted (a standalone script, most CLI usage) just means a
|
|
73
|
+
* pending question can't be session-scoped — still fine, it's still
|
|
74
|
+
* agent-scoped either way. */
|
|
75
|
+
sessionId?: string;
|
|
76
|
+
/** Default Approver for this call's 'ask' decisions, when
|
|
77
|
+
* AgentConfig.approver isn't set — the seam an adapter uses to pick
|
|
78
|
+
* whichever approver actually fits its own channel (ConsoleApprover for
|
|
79
|
+
* a real terminal, a web-based one for an HTTP-served conversation,
|
|
80
|
+
* ...) without every agent needing to hardcode one itself. An explicit
|
|
81
|
+
* AgentConfig.approver still wins outright over this when the agent
|
|
82
|
+
* author set one on purpose — same as it always has (see Gate
|
|
83
|
+
* construction below) — this only fills in when that's absent. Default,
|
|
84
|
+
* if neither is given: actauth's own ConsoleApprover, same as always. */
|
|
85
|
+
approver?: Approver;
|
|
86
|
+
/** Fired the instant the system ask_user tool registers a new pending
|
|
87
|
+
* question — this is the seam that decides between a real, answerable
|
|
88
|
+
* pending question (something adapters/http.ts's own /questions REST
|
|
89
|
+
* endpoints, or an SSE push, can resolve later) and a blocking terminal
|
|
90
|
+
* prompt with nowhere else to go (see system-tools/ask_user.ts's own
|
|
91
|
+
* promptOnConsole).
|
|
92
|
+
*
|
|
93
|
+
* Deliberately its own option, not inferred from onEvent above —
|
|
94
|
+
* onEvent means "please tell me about loop events" (adapters/cli.ts
|
|
95
|
+
* passes one purely to log them), not "I have a way to actually answer
|
|
96
|
+
* a question later." Conflating the two was a real bug: it made a
|
|
97
|
+
* plain `loopengine run` hang forever registering an unanswerable
|
|
98
|
+
* question instead of just prompting in the terminal it's already
|
|
99
|
+
* attached to — confirmed live before this existed. Only an adapter
|
|
100
|
+
* that genuinely has an answering mechanism (adapters/http.ts, for both
|
|
101
|
+
* its routes) should pass this. */
|
|
102
|
+
onQuestionPending?: (question: PendingQuestion) => void;
|
|
49
103
|
}
|
|
50
104
|
export interface RunAgentResult {
|
|
51
105
|
/** Final assistant text for this turn. */
|
|
@@ -55,17 +109,31 @@ export interface RunAgentResult {
|
|
|
55
109
|
* the caller: store/transmit it, don't inspect or mutate it. */
|
|
56
110
|
history: Message[];
|
|
57
111
|
/** Exactly what this turn added — safe to durably append regardless of
|
|
58
|
-
* whether
|
|
112
|
+
* whether compaction.ts's own recovery reshaped `history` mid-turn. A caller
|
|
59
113
|
* that persists conversations (see session-store.ts) should use this
|
|
60
114
|
* instead of diffing `history` by length against what it loaded: that
|
|
61
115
|
* only works if `history` only ever grows, which stops being true the
|
|
62
116
|
* moment recovery can shrink/rewrite it, not just extend it. */
|
|
63
117
|
newMessages: Message[];
|
|
64
|
-
/** Set
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
118
|
+
/** Set when the loop stopped for a reason other than the model
|
|
119
|
+
* producing a final answer with no more tool_use blocks — `text` in
|
|
120
|
+
* either case is a synthetic notice, not something the model said.
|
|
121
|
+
* Absent on a normal finish.
|
|
122
|
+
*
|
|
123
|
+
* 'max_turns': hit config.maxTurns without a final answer.
|
|
124
|
+
*
|
|
125
|
+
* 'denied': a human denied at least one requested tool call this turn
|
|
126
|
+
* (see actauth's own 'ask'/'deny' decisions) — the loop stops right
|
|
127
|
+
* there instead of feeding "denied: ..." back to the model and letting
|
|
128
|
+
* it keep going on its own, the same way Claude Code itself stops an
|
|
129
|
+
* entire pending batch rather than quietly carrying out the parts you
|
|
130
|
+
* didn't object to. A denial cancels the *whole* turn's batch, not
|
|
131
|
+
* just the call(s) that were themselves denied — any other call the
|
|
132
|
+
* model requested in the same turn, even one already approved, never
|
|
133
|
+
* runs either (see the loop's own body); it gets a "skipped", not a
|
|
134
|
+
* "denied", tool_result, since it was never evaluated against its own
|
|
135
|
+
* rule. */
|
|
136
|
+
stopReason?: 'max_turns' | 'denied';
|
|
69
137
|
}
|
|
70
138
|
/** Resolves AgentConfig.rules the same way skillsDirs resolves its own
|
|
71
139
|
* default: an inline array is used as-is (full 3-segment scopePattern,
|