loopengine 0.0.17 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +67 -6
  2. package/core/system-skills/composio-large-outputs/SKILL.md +25 -0
  3. package/dist/{cli.d.ts → bin/cli.d.ts} +27 -6
  4. package/dist/bin/cli.js +384 -0
  5. package/dist/{agent-as-tool.d.ts → core/agent-as-tool.d.ts} +2 -2
  6. package/dist/{agent-as-tool.js → core/agent-as-tool.js} +1 -1
  7. package/dist/core/client.d.ts +113 -0
  8. package/dist/core/client.js +123 -0
  9. package/dist/core/discover-agents.d.ts +45 -0
  10. package/dist/{discover-agents.js → core/discover-agents.js} +12 -4
  11. package/dist/core/gateway-tools.d.ts +226 -0
  12. package/dist/core/gateway-tools.js +660 -0
  13. package/dist/core/loop-events.d.ts +168 -0
  14. package/dist/core/loop-events.js +1 -0
  15. package/dist/{model-calls → core/model-calls}/anthropic-model-call.d.ts +1 -1
  16. package/dist/{model-calls → core/model-calls}/deepseek-model-call.d.ts +1 -1
  17. package/dist/{model-calls → core/model-calls}/openai-model-call.d.ts +1 -1
  18. package/dist/core/run-agent.d.ts +182 -0
  19. package/dist/{run-agent.js → core/run-agent.js} +233 -51
  20. package/dist/{session-store.d.ts → core/session-store.d.ts} +13 -1
  21. package/dist/{session-store.js → core/session-store.js} +19 -0
  22. package/dist/core/system-tools/ask_user.d.ts +72 -0
  23. package/dist/core/system-tools/ask_user.js +136 -0
  24. package/dist/core/system-tools/index.d.ts +3 -0
  25. package/dist/core/system-tools/index.js +12 -0
  26. package/dist/core/system-tools/read_file.d.ts +28 -0
  27. package/dist/core/system-tools/read_file.js +64 -0
  28. package/dist/index.d.ts +19 -16
  29. package/dist/index.js +16 -9
  30. package/package.json +33 -25
  31. package/dist/cli.js +0 -191
  32. package/dist/discover-agents.d.ts +0 -24
  33. package/dist/run-agent.d.ts +0 -92
  34. /package/dist/{agent-config.d.ts → core/agent-config.d.ts} +0 -0
  35. /package/dist/{agent-config.js → core/agent-config.js} +0 -0
  36. /package/dist/{model-calls → core/model-calls}/anthropic-model-call.js +0 -0
  37. /package/dist/{model-calls → core/model-calls}/deepseek-model-call.js +0 -0
  38. /package/dist/{model-calls → core/model-calls}/openai-model-call.js +0 -0
  39. /package/dist/{vector-index.d.ts → core/vector-index.d.ts} +0 -0
  40. /package/dist/{vector-index.js → core/vector-index.js} +0 -0
@@ -0,0 +1,168 @@
1
+ import type { CheckResult } from 'contextclip';
2
+ import type { RecoveryAction } from 'reflowkit';
3
+ import type { Decision, PendingApproval } from 'actauth';
4
+ import type { PendingQuestion } from './system-tools/index.js';
5
+ /** ContextClipper's own read-only budget check, run once per loop turn
6
+ * before the model call — reported unchanged (spread, not renamed) so
7
+ * this never drifts from whatever contextclip's own CheckResult actually
8
+ * contains. */
9
+ export type ContextClipCheckEvent = {
10
+ type: 'contextclip:check';
11
+ } & CheckResult;
12
+ /** ContextClip recovery kicked in because the prompt was rejected as too
13
+ * large — `from`/`to` are message-array lengths (this turn's own
14
+ * newMessages included), not token counts; see run-agent.ts's own
15
+ * reflow.call onPromptTooLong hook. */
16
+ export interface ReflowRecoverEvent {
17
+ type: 'reflow:recover';
18
+ from: number;
19
+ to: number;
20
+ }
21
+ /** Ordered log of whatever reflowkit's own Reflow.call had to do to get a
22
+ * usable response this turn (prompt-too-long retries, truncation
23
+ * retries, ...) — empty-array case never fires this event at all (see
24
+ * run-agent.ts: only logged when recoveries.length > 0). */
25
+ export interface ReflowRecoveriesEvent {
26
+ type: 'reflow:recoveries';
27
+ recoveries: RecoveryAction[];
28
+ }
29
+ /** The model's own "I'll do X" preamble alongside a tool_use request —
30
+ * the only *live* signal of assistant text mid-turn (the final answer
31
+ * only ever arrives via 'loop:done'/'done', once, at the very end). */
32
+ export interface AssistantTextEvent {
33
+ type: 'assistant:text';
34
+ text: string;
35
+ }
36
+ /** One ActAuth Gate.evaluate() outcome per requested tool call — 'allow'
37
+ * here can still mean a human was asked interactively and said yes (see
38
+ * run-agent.ts's own wasAskedInteractively); this event fires regardless,
39
+ * the interactive case just also gets its own approval:pending. */
40
+ export interface ActauthDecisionEvent {
41
+ type: 'actauth:decision';
42
+ tool: string;
43
+ decision: Decision;
44
+ reason: string;
45
+ }
46
+ /** A tool call's decision is in and it's about to run (or, for a system
47
+ * tool, already bypassed the gate entirely) — fired the instant that's
48
+ * known, not once execution finishes, so a live caller can show
49
+ * "Running…" immediately instead of a silent gap. Matched to its later
50
+ * 'tool:result' by `id` (the model's own tool_use id). */
51
+ export interface ToolStartedEvent {
52
+ type: 'tool:started';
53
+ id: string;
54
+ tool: string;
55
+ args: Record<string, unknown>;
56
+ detailText: string;
57
+ }
58
+ /** A tool call's resolution — auto-allowed, interactively approved,
59
+ * denied, or skipped alike. `args`/`detailText` are omitted for some
60
+ * skipped/denied paths that never had them resolved (see run-agent.ts's
61
+ * own call sites). */
62
+ export interface ToolResultEvent {
63
+ type: 'tool:result';
64
+ id: string;
65
+ tool: string;
66
+ args?: Record<string, unknown>;
67
+ detailText?: string;
68
+ statusText: string;
69
+ }
70
+ /** ToolLane's own raw fulfil/reject outcome, logged alongside (not
71
+ * instead of) the richer 'tool:result' above — kept as its own event
72
+ * since it's toollane's own vocabulary (`summary`, not `statusText`), not
73
+ * reshaped to match. */
74
+ export interface ToolLaneResultEvent {
75
+ type: 'toollane:result';
76
+ name: string;
77
+ summary: string;
78
+ }
79
+ /** A `Skill` tool_use block was resolved to a real skill body via
80
+ * SkillGarden — not gated by ActAuth (see run-agent.ts's own comment on
81
+ * why Skill invocation isn't a real tool call). */
82
+ export interface SkillGardenInvokeEvent {
83
+ type: 'skillgarden:invoke';
84
+ skill: string;
85
+ }
86
+ /** An approved call never ran because a sibling call in the same turn's
87
+ * batch was denied — the whole batch cancels together (see
88
+ * RunAgentResult.stopReason's own doc comment). */
89
+ export interface LoopSkippedEvent {
90
+ type: 'loop:skipped';
91
+ name: string;
92
+ deniedTools: string[];
93
+ }
94
+ /** Hit config.maxTurns without the model producing a final answer. */
95
+ export interface LoopMaxTurnsEvent {
96
+ type: 'loop:max_turns';
97
+ maxTurns: number;
98
+ }
99
+ /** A human denied at least one requested tool call this turn — the whole
100
+ * turn stops right there (see RunAgentResult.stopReason). */
101
+ export interface LoopDeniedEvent {
102
+ type: 'loop:denied';
103
+ deniedTools: string[];
104
+ }
105
+ /** The loop's own final answer — the model produced text with no more
106
+ * tool_use blocks. Distinct from the adapter-level 'done' below: this
107
+ * only ever means a genuine finish, never a synthetic max_turns/denied
108
+ * notice (see adapters/http.ts's own 'done', which covers all three). */
109
+ export interface LoopDoneEvent {
110
+ type: 'loop:done';
111
+ text: string;
112
+ }
113
+ /** Every event runAgent() itself can emit via RunAgentOptions.onEvent —
114
+ * not the adapter-synthesized ones below, which no caller of runAgent()
115
+ * itself ever produces. */
116
+ export type RunAgentLoopEvent = ContextClipCheckEvent | ReflowRecoverEvent | ReflowRecoveriesEvent | AssistantTextEvent | ActauthDecisionEvent | ToolStartedEvent | ToolResultEvent | ToolLaneResultEvent | SkillGardenInvokeEvent | LoopSkippedEvent | LoopMaxTurnsEvent | LoopDeniedEvent | LoopDoneEvent;
117
+ /** adapters/http.ts echoing back which session id this turn used — first
118
+ * event of every streamed turn, so a caller that omitted sessionId can
119
+ * capture what got generated. Not something runAgent() itself knows
120
+ * (session ids are resolved per-request by the adapter, before runAgent
121
+ * is even called). */
122
+ export interface SessionEvent {
123
+ type: 'session';
124
+ sessionId: string;
125
+ }
126
+ /** A tool call needs a human 'ask' decision, live — spreads actauth's own
127
+ * PendingApproval verbatim (id/tool/args/scope/reason/requestedAt)
128
+ * alongside `type`, so this never drifts from whatever that package
129
+ * actually returns. */
130
+ export type ApprovalPendingEvent = {
131
+ type: 'approval:pending';
132
+ } & PendingApproval;
133
+ /** The system ask_user tool raised a genuinely ambiguous question — same
134
+ * spread-verbatim reasoning as ApprovalPendingEvent above, against
135
+ * system-tools/ask_user.ts's own PendingQuestion. */
136
+ export type QuestionPendingEvent = {
137
+ type: 'question:pending';
138
+ } & PendingQuestion;
139
+ /** The turn is over — covers a genuine finish and both synthetic
140
+ * stopReasons (max_turns/denied) alike, the one event every caller can
141
+ * treat as "nothing more is coming" regardless of which of the three it
142
+ * was (see RunAgentResult.stopReason's own doc comment). */
143
+ export interface DoneEvent {
144
+ type: 'done';
145
+ text: string;
146
+ stopReason?: 'max_turns' | 'denied';
147
+ }
148
+ /** Something failed after the response was already committed (SSE
149
+ * headers sent, or a background turn already running) — surfaced in-band
150
+ * rather than as an HTTP status code, since it's too late for one. */
151
+ export interface ErrorEvent {
152
+ type: 'error';
153
+ error: string;
154
+ }
155
+ /** Synthesized by adapters/http.ts around a turn — session ids and
156
+ * HTTP-level errors aren't things runAgent() itself produces, but they're
157
+ * still part of the same lifecycle a caller observes end to end. */
158
+ export type AdapterLoopEvent = SessionEvent | ApprovalPendingEvent | QuestionPendingEvent | DoneEvent | ErrorEvent;
159
+ /** The full wire vocabulary — what run-agent.ts emits via onEvent, plus
160
+ * what adapters/http.ts (and, in --json mode, adapters/cli.ts) layers
161
+ * around it. This is "the standard output" every channel adapter is a
162
+ * projection of: adapters/http.ts's SSE stream writes one of these per
163
+ * frame; its plain /messages route collects the whole array into an
164
+ * `events` response field; adapters/cli.ts's --json mode writes one per
165
+ * NDJSON line; web/playground.ts is just a UI rendering whichever of
166
+ * those two HTTP routes it's talking to. */
167
+ export type LoopEvent = RunAgentLoopEvent | AdapterLoopEvent;
168
+ 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 { ModelCall } from '#run-agent.js';
2
+ import type { ModelCall } from '#core/run-agent.js';
3
3
  export interface DeepSeekModelCallOptions {
4
4
  /** Defaults to the DEEPSEEK_API_KEY env var. */
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. */
@@ -0,0 +1,182 @@
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[];
9
+ export interface ModelContentBlock {
10
+ type: string;
11
+ /** text blocks. */
12
+ text?: string;
13
+ /** tool_use blocks: this call's own id (referenced by a later tool_result). */
14
+ id?: string;
15
+ /** tool_use blocks. */
16
+ name?: string;
17
+ /** tool_use blocks. */
18
+ input?: Record<string, unknown>;
19
+ /** tool_result blocks: id of the tool_use block this result answers. */
20
+ tool_use_id?: string;
21
+ /** tool_result blocks: the tool's output (or denial/error message). */
22
+ content?: string;
23
+ /** tool_result blocks: true if the tool call failed or was denied. */
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;
32
+ }
33
+ export interface ModelResponse {
34
+ stop_reason: string;
35
+ content: ModelContentBlock[];
36
+ }
37
+ /** loopengine's own conversation-message type — a superset of
38
+ * ContextClip's `{role, content: string}`: `content` can also be a
39
+ * `ModelContentBlock[]`, the same block shape `ModelResponse.content`
40
+ * already used, so a model's tool_use requests and this loop's
41
+ * tool_result replies round-trip with real per-call identity
42
+ * (`tool_use_id`) instead of being flattened into prose. Plain-string
43
+ * content (a user's message, a skill's injected body, a synthetic
44
+ * compaction note) is just as valid — nothing requires every message to
45
+ * be block-structured. */
46
+ export interface Message {
47
+ role: string;
48
+ content: string | ModelContentBlock[];
49
+ }
50
+ /** Swap the Anthropic SDK in here — this is the only seam a real API call needs. */
51
+ export type ModelCall = (messages: Message[], system: string, tools: ToolSchema[]) => Promise<ModelResponse>;
52
+ export interface RunAgentOptions {
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;
57
+ /** The ActAuth tenant this call runs as — feeds the Gate's scope, so it
58
+ * governs which rules apply. runAgent() never sees a request, so it
59
+ * can't call AgentConfig.tenantFor itself; adapters/http.ts resolves it
60
+ * per request and passes the result here. Default `'default'`, same as
61
+ * omitting it — every standalone/CLI caller (which never has a request
62
+ * to resolve tenantFor from) gets that default automatically. */
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;
103
+ }
104
+ export interface RunAgentResult {
105
+ /** Final assistant text for this turn. */
106
+ text: string;
107
+ /** Full conversation so far, including this turn — pass back in as
108
+ * `history` on the next call to continue the conversation. Opaque to
109
+ * the caller: store/transmit it, don't inspect or mutate it. */
110
+ history: Message[];
111
+ /** Exactly what this turn added — safe to durably append regardless of
112
+ * whether ContextClip recovery reshaped `history` mid-turn. A caller
113
+ * that persists conversations (see session-store.ts) should use this
114
+ * instead of diffing `history` by length against what it loaded: that
115
+ * only works if `history` only ever grows, which stops being true the
116
+ * moment recovery can shrink/rewrite it, not just extend it. */
117
+ newMessages: Message[];
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';
137
+ }
138
+ /** Resolves AgentConfig.rules the same way skillsDirs resolves its own
139
+ * default: an inline array is used as-is (full 3-segment scopePattern,
140
+ * unchanged — see loadYamlRules for why only the YAML form gets the
141
+ * agent segment auto-appended); omitted entirely defaults to
142
+ * `agents/<name>/actauth.yml`, this repo's own folder-form convention.
143
+ * Unlike skillsDirs, a missing *default* file falls back to an empty
144
+ * ruleset rather than propagating the raw ENOENT — permissions failing
145
+ * shouldn't crash the whole call. That fallback defaults to `'deny'`, not
146
+ * the inline-array form's `'ask'`: no `actauth.yml` at all means this
147
+ * agent's permission story was never actually written, which is a
148
+ * stricter unknown than "written, but this one tool has no rule" — 'ask'
149
+ * still runs an Approver that could auto-approve, while 'deny' refuses
150
+ * outright until real rules exist. `defaultDecision`, if set, still wins
151
+ * over that. An *explicitly* given path that doesn't exist is a real
152
+ * configuration bug, though, and still throws — only the silent, implicit
153
+ * default gets this forgiveness.
154
+ *
155
+ * Exported (alongside loadDefaultTools below) so adapters/http.ts's
156
+ * GET /agents/:name/config route can show the same rules runAgent() would
157
+ * actually enforce, not a re-derived guess at them. */
158
+ export declare function loadRules(config: AgentConfig): RuleSet;
159
+ export declare function loadDefaultTools(config: AgentConfig): Promise<ToolDefinition[]>;
160
+ /** Auto-loads agents/<name>/subagents/<child>/index.{ts,js} — each one a
161
+ * full AgentConfig, wrapped with agentAsTool and merged into `config`'s
162
+ * own tools, no import or AgentConfig.tools edit required. Unlike
163
+ * loadDefaultTools, this always runs regardless of whether `config.tools`
164
+ * was left to its own default or set explicitly — see AgentConfig.tools's
165
+ * own doc comment for why subagents are a distinct concern from
166
+ * hand-written tools. A missing subagents/ folder is just `[]`, same
167
+ * missing-is-fine treatment tools/ and skills/ get; a subdirectory with
168
+ * neither index.ts nor index.js is skipped, same as resolveModulePath's
169
+ * own handling in discover-agents.ts.
170
+ *
171
+ * Each subagent is loaded via loadAgentModule — the same per-module
172
+ * resolution discoverAgents itself uses — then patched by
173
+ * resolveSubagentConfig (see its own doc comment for why) before being
174
+ * wrapped. Its own `config` goes through this exact same function again
175
+ * the next time *it* runs (inside agentAsTool's execute, via runAgent).
176
+ * That's what makes nesting (a subagent with its own subagents/ folder)
177
+ * work with no extra recursion code: it's just this function being
178
+ * called again, one level down. A folder can't be its own ancestor, so
179
+ * this can't cycle the way a hand-wired agentAsTool(getEntry(...)) call
180
+ * elsewhere could. */
181
+ export declare function loadSubagentAsTools(config: AgentConfig): Promise<ToolDefinition[]>;
182
+ export declare function runAgent(config: AgentConfig, modelCall: ModelCall, userMessage: string, history?: Message[], options?: RunAgentOptions): Promise<RunAgentResult>;