@oharness/engine 0.1.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/dist/agent/agent.d.ts +196 -0
- package/dist/agent/agent.js +758 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/compaction.d.ts +68 -0
- package/dist/agent/compaction.js +179 -0
- package/dist/agent/compaction.js.map +1 -0
- package/dist/agent/hooks.d.ts +77 -0
- package/dist/agent/hooks.js +152 -0
- package/dist/agent/hooks.js.map +1 -0
- package/dist/agent/memory.d.ts +45 -0
- package/dist/agent/memory.js +147 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/overflow.d.ts +35 -0
- package/dist/agent/overflow.js +69 -0
- package/dist/agent/overflow.js.map +1 -0
- package/dist/agent/permissions.d.ts +181 -0
- package/dist/agent/permissions.js +289 -0
- package/dist/agent/permissions.js.map +1 -0
- package/dist/agent/prompt.d.ts +25 -0
- package/dist/agent/prompt.js +63 -0
- package/dist/agent/prompt.js.map +1 -0
- package/dist/agent/session.d.ts +108 -0
- package/dist/agent/session.js +231 -0
- package/dist/agent/session.js.map +1 -0
- package/dist/agent/settings.d.ts +37 -0
- package/dist/agent/settings.js +113 -0
- package/dist/agent/settings.js.map +1 -0
- package/dist/agent/skills.d.ts +58 -0
- package/dist/agent/skills.js +225 -0
- package/dist/agent/skills.js.map +1 -0
- package/dist/agent/store.d.ts +308 -0
- package/dist/agent/store.js +596 -0
- package/dist/agent/store.js.map +1 -0
- package/dist/agent/subagent.d.ts +44 -0
- package/dist/agent/subagent.js +138 -0
- package/dist/agent/subagent.js.map +1 -0
- package/dist/agent/titles.d.ts +8 -0
- package/dist/agent/titles.js +23 -0
- package/dist/agent/titles.js.map +1 -0
- package/dist/agent/workspace.d.ts +60 -0
- package/dist/agent/workspace.js +156 -0
- package/dist/agent/workspace.js.map +1 -0
- package/dist/artifacts/publish.d.ts +94 -0
- package/dist/artifacts/publish.js +104 -0
- package/dist/artifacts/publish.js.map +1 -0
- package/dist/artifacts/store.d.ts +324 -0
- package/dist/artifacts/store.js +500 -0
- package/dist/artifacts/store.js.map +1 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/index.js +10 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/manager.d.ts +83 -0
- package/dist/auth/manager.js +266 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/oauth.d.ts +83 -0
- package/dist/auth/oauth.js +327 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/providers/account.d.ts +59 -0
- package/dist/auth/providers/account.js +282 -0
- package/dist/auth/providers/account.js.map +1 -0
- package/dist/auth/providers/anthropic.d.ts +24 -0
- package/dist/auth/providers/anthropic.js +209 -0
- package/dist/auth/providers/anthropic.js.map +1 -0
- package/dist/auth/providers/generic.d.ts +57 -0
- package/dist/auth/providers/generic.js +155 -0
- package/dist/auth/providers/generic.js.map +1 -0
- package/dist/auth/providers/official.d.ts +3 -0
- package/dist/auth/providers/official.js +90 -0
- package/dist/auth/providers/official.js.map +1 -0
- package/dist/auth/providers/ottoport.d.ts +3 -0
- package/dist/auth/providers/ottoport.js +60 -0
- package/dist/auth/providers/ottoport.js.map +1 -0
- package/dist/auth/store.d.ts +22 -0
- package/dist/auth/store.js +138 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/auth/types.d.ts +167 -0
- package/dist/auth/types.js +16 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/config.d.ts +197 -0
- package/dist/config.js +195 -0
- package/dist/config.js.map +1 -0
- package/dist/core/errors.d.ts +46 -0
- package/dist/core/errors.js +83 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/tokens.d.ts +12 -0
- package/dist/core/tokens.js +56 -0
- package/dist/core/tokens.js.map +1 -0
- package/dist/core/types.d.ts +91 -0
- package/dist/core/types.js +49 -0
- package/dist/core/types.js.map +1 -0
- package/dist/harness.d.ts +302 -0
- package/dist/harness.js +695 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +199 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manager.d.ts +77 -0
- package/dist/mcp/manager.js +325 -0
- package/dist/mcp/manager.js.map +1 -0
- package/dist/mcp/oauth.d.ts +63 -0
- package/dist/mcp/oauth.js +302 -0
- package/dist/mcp/oauth.js.map +1 -0
- package/dist/mcp/protocol.d.ts +130 -0
- package/dist/mcp/protocol.js +14 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/registry.d.ts +52 -0
- package/dist/mcp/registry.js +100 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/registry.json +293 -0
- package/dist/mcp/secrets.d.ts +20 -0
- package/dist/mcp/secrets.js +75 -0
- package/dist/mcp/secrets.js.map +1 -0
- package/dist/mcp/transport.d.ts +51 -0
- package/dist/mcp/transport.js +158 -0
- package/dist/mcp/transport.js.map +1 -0
- package/dist/paths.d.ts +37 -0
- package/dist/paths.js +63 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +201 -0
- package/dist/plugins.js.map +1 -0
- package/dist/providers/builtin.d.ts +23 -0
- package/dist/providers/builtin.js +141 -0
- package/dist/providers/builtin.js.map +1 -0
- package/dist/providers/catalog.d.ts +129 -0
- package/dist/providers/catalog.js +254 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/catalog.json +472 -0
- package/dist/providers/discovery.d.ts +16 -0
- package/dist/providers/discovery.js +161 -0
- package/dist/providers/discovery.js.map +1 -0
- package/dist/providers/http.d.ts +24 -0
- package/dist/providers/http.js +139 -0
- package/dist/providers/http.js.map +1 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/piai.d.ts +124 -0
- package/dist/providers/piai.js +551 -0
- package/dist/providers/piai.js.map +1 -0
- package/dist/providers/registry.d.ts +65 -0
- package/dist/providers/registry.js +195 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/resilient.d.ts +46 -0
- package/dist/providers/resilient.js +117 -0
- package/dist/providers/resilient.js.map +1 -0
- package/dist/providers/types.d.ts +182 -0
- package/dist/providers/types.js +50 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/schedule/cron.d.ts +35 -0
- package/dist/schedule/cron.js +125 -0
- package/dist/schedule/cron.js.map +1 -0
- package/dist/schedule/scheduler.d.ts +87 -0
- package/dist/schedule/scheduler.js +237 -0
- package/dist/schedule/scheduler.js.map +1 -0
- package/dist/storage/kv.d.ts +56 -0
- package/dist/storage/kv.js +173 -0
- package/dist/storage/kv.js.map +1 -0
- package/dist/storage/migrate.d.ts +11 -0
- package/dist/storage/migrate.js +93 -0
- package/dist/storage/migrate.js.map +1 -0
- package/dist/storage/project.d.ts +9 -0
- package/dist/storage/project.js +57 -0
- package/dist/storage/project.js.map +1 -0
- package/dist/storage/records.d.ts +32 -0
- package/dist/storage/records.js +48 -0
- package/dist/storage/records.js.map +1 -0
- package/dist/sync/client.d.ts +79 -0
- package/dist/sync/client.js +118 -0
- package/dist/sync/client.js.map +1 -0
- package/dist/sync/mirror.d.ts +85 -0
- package/dist/sync/mirror.js +389 -0
- package/dist/sync/mirror.js.map +1 -0
- package/dist/sync/protocol.d.ts +67 -0
- package/dist/sync/protocol.js +16 -0
- package/dist/sync/protocol.js.map +1 -0
- package/dist/sync/settings.d.ts +56 -0
- package/dist/sync/settings.js +60 -0
- package/dist/sync/settings.js.map +1 -0
- package/dist/tools/builtin/artifact.d.ts +16 -0
- package/dist/tools/builtin/artifact.js +134 -0
- package/dist/tools/builtin/artifact.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts +3 -0
- package/dist/tools/builtin/bash.js +142 -0
- package/dist/tools/builtin/bash.js.map +1 -0
- package/dist/tools/builtin/fs.d.ts +19 -0
- package/dist/tools/builtin/fs.js +280 -0
- package/dist/tools/builtin/fs.js.map +1 -0
- package/dist/tools/builtin/index.d.ts +22 -0
- package/dist/tools/builtin/index.js +42 -0
- package/dist/tools/builtin/index.js.map +1 -0
- package/dist/tools/builtin/mcp-resource.d.ts +3 -0
- package/dist/tools/builtin/mcp-resource.js +220 -0
- package/dist/tools/builtin/mcp-resource.js.map +1 -0
- package/dist/tools/builtin/memory.d.ts +10 -0
- package/dist/tools/builtin/memory.js +76 -0
- package/dist/tools/builtin/memory.js.map +1 -0
- package/dist/tools/builtin/recall.d.ts +13 -0
- package/dist/tools/builtin/recall.js +163 -0
- package/dist/tools/builtin/recall.js.map +1 -0
- package/dist/tools/builtin/search.d.ts +3 -0
- package/dist/tools/builtin/search.js +240 -0
- package/dist/tools/builtin/search.js.map +1 -0
- package/dist/tools/builtin/skill.d.ts +10 -0
- package/dist/tools/builtin/skill.js +46 -0
- package/dist/tools/builtin/skill.js.map +1 -0
- package/dist/tools/builtin/task.d.ts +9 -0
- package/dist/tools/builtin/task.js +68 -0
- package/dist/tools/builtin/task.js.map +1 -0
- package/dist/tools/builtin/todo.d.ts +20 -0
- package/dist/tools/builtin/todo.js +63 -0
- package/dist/tools/builtin/todo.js.map +1 -0
- package/dist/tools/define.d.ts +19 -0
- package/dist/tools/define.js +39 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +39 -0
- package/dist/tools/registry.js +101 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +112 -0
- package/dist/tools/types.js +22 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/util/id.d.ts +9 -0
- package/dist/util/id.js +19 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/json.d.ts +25 -0
- package/dist/util/json.js +100 -0
- package/dist/util/json.js.map +1 -0
- package/dist/util/jwt.d.ts +74 -0
- package/dist/util/jwt.js +177 -0
- package/dist/util/jwt.js.map +1 -0
- package/dist/util/logger.d.ts +13 -0
- package/dist/util/logger.js +59 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/queue.d.ts +26 -0
- package/dist/util/queue.js +106 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/schema.d.ts +17 -0
- package/dist/util/schema.js +98 -0
- package/dist/util/schema.js.map +1 -0
- package/dist/util/shell.d.ts +20 -0
- package/dist/util/shell.js +24 -0
- package/dist/util/shell.js.map +1 -0
- package/official.json +4 -0
- package/package.json +38 -0
- package/skills/artifact-authoring/SKILL.md +81 -0
- package/skills/canvas/SKILL.md +85 -0
- package/skills/skill-authoring/SKILL.md +85 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { errorMessage } from "../core/errors.js";
|
|
2
|
+
import { estimateStringTokens } from "../core/tokens.js";
|
|
3
|
+
import { assistantMessage, emptyUsage, messageText, userMessage } from "../core/types.js";
|
|
4
|
+
import { createTaskTool } from "../tools/builtin/task.js";
|
|
5
|
+
import { ARCHIVE_READER } from "../tools/builtin/recall.js";
|
|
6
|
+
import { ARTIFACT_METADATA_KEY } from "../tools/builtin/artifact.js";
|
|
7
|
+
import { previewJson } from "../util/json.js";
|
|
8
|
+
import { AsyncQueue, mergeStreams } from "../util/queue.js";
|
|
9
|
+
import { createLogger } from "../util/logger.js";
|
|
10
|
+
import { compact, shouldCompact } from "./compaction.js";
|
|
11
|
+
import { capToolResult } from "./overflow.js";
|
|
12
|
+
import { checkWrite, resolveWithin } from "./workspace.js";
|
|
13
|
+
import { SkillRegistry } from "./skills.js";
|
|
14
|
+
import { MemoryStore } from "./memory.js";
|
|
15
|
+
import { createMemoryTools } from "../tools/builtin/memory.js";
|
|
16
|
+
import { createSkillTool } from "../tools/builtin/skill.js";
|
|
17
|
+
import { createMcpResourceTools } from "../tools/builtin/mcp-resource.js";
|
|
18
|
+
import { HookRegistry } from "./hooks.js";
|
|
19
|
+
import { buildSystemPrompt } from "./prompt.js";
|
|
20
|
+
import { SubagentRegistry } from "./subagent.js";
|
|
21
|
+
const DEFAULT_MAX_TURNS = 60;
|
|
22
|
+
/**
|
|
23
|
+
* The agentic loop: prompt the model, run whatever tools it asks for, feed the
|
|
24
|
+
* results back, repeat until it stops calling tools.
|
|
25
|
+
*
|
|
26
|
+
* Everything provider-specific lives behind `Provider`, so this loop is the
|
|
27
|
+
* same regardless of which model is selected — including when the model
|
|
28
|
+
* changes partway through a session.
|
|
29
|
+
*/
|
|
30
|
+
export class Agent {
|
|
31
|
+
session;
|
|
32
|
+
tools;
|
|
33
|
+
hooks;
|
|
34
|
+
subagents;
|
|
35
|
+
skills;
|
|
36
|
+
memory;
|
|
37
|
+
state;
|
|
38
|
+
opts;
|
|
39
|
+
logger;
|
|
40
|
+
agentId;
|
|
41
|
+
controller = new AbortController();
|
|
42
|
+
running = false;
|
|
43
|
+
sessionStarted = false;
|
|
44
|
+
/** Set when a hook asks the turn to end; read by the main loop. */
|
|
45
|
+
stopRequested;
|
|
46
|
+
/** Memory index for this run's system prompt. */
|
|
47
|
+
memoryIndex;
|
|
48
|
+
constructor(opts) {
|
|
49
|
+
this.opts = opts;
|
|
50
|
+
this.session = opts.session;
|
|
51
|
+
this.tools = opts.tools;
|
|
52
|
+
this.hooks = opts.hooks ?? new HookRegistry();
|
|
53
|
+
this.subagents = opts.subagents ?? new SubagentRegistry();
|
|
54
|
+
this.skills = opts.skills ?? new SkillRegistry();
|
|
55
|
+
this.memory = opts.memory;
|
|
56
|
+
this.state = opts.session.state;
|
|
57
|
+
this.agentId = opts.agentId ?? "main";
|
|
58
|
+
this.logger = (opts.logger ?? createLogger("agent")).child(this.agentId);
|
|
59
|
+
// Let `recall` reach this session's archive. Bound here rather than passed
|
|
60
|
+
// through ToolContext so subagents sharing the session see it too.
|
|
61
|
+
this.state.set(ARCHIVE_READER, () => this.session.archive());
|
|
62
|
+
// MCP resources and prompts are only reachable through tools, and only
|
|
63
|
+
// worth advertising when a server is actually connected.
|
|
64
|
+
if (this.opts.mcp && !this.tools.has("mcp_read_resource")) {
|
|
65
|
+
const connected = this.opts.mcp.status().some((server) => server.connected);
|
|
66
|
+
if (connected)
|
|
67
|
+
this.tools.registerAll(createMcpResourceTools(this.opts.mcp));
|
|
68
|
+
}
|
|
69
|
+
if (this.memory && !this.tools.has("memory")) {
|
|
70
|
+
this.tools.registerAll(createMemoryTools(this.memory));
|
|
71
|
+
}
|
|
72
|
+
// Same reasoning as the task tool: the description has to name the skills
|
|
73
|
+
// that exist, or the model has nothing to match a task against.
|
|
74
|
+
if (!this.tools.has("skill") && this.skills.size > 0) {
|
|
75
|
+
this.tools.register(createSkillTool(this.skills));
|
|
76
|
+
}
|
|
77
|
+
// The task tool has to be built here: its description enumerates the
|
|
78
|
+
// subagent types that actually exist in this configuration.
|
|
79
|
+
if (!this.tools.has("task") && this.subagents.list().length > 0) {
|
|
80
|
+
this.tools.register(createTaskTool(this));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Announce a model switch so a UI can render it and hooks can react.
|
|
85
|
+
* Called by whoever changes the session's model.
|
|
86
|
+
*/
|
|
87
|
+
async *switchModel(ref) {
|
|
88
|
+
const { from, to } = this.session.setModel(ref);
|
|
89
|
+
yield { type: "model_changed", from: from.id, to: to.id };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The model used for compaction summaries. Falls back to the session model
|
|
93
|
+
* if `smallModel` is unset or unresolvable — a bad alias should degrade to a
|
|
94
|
+
* pricier summary, never break the turn.
|
|
95
|
+
*/
|
|
96
|
+
summarizerModel() {
|
|
97
|
+
const ref = this.opts.smallModel;
|
|
98
|
+
if (!ref)
|
|
99
|
+
return this.session.model;
|
|
100
|
+
try {
|
|
101
|
+
return this.session.providers.resolve(ref);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
this.logger.warn("smallModel could not be resolved; using the session model", { ref });
|
|
105
|
+
return this.session.model;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Run the SessionEnd hook. Call when the session is finished with. */
|
|
109
|
+
async end() {
|
|
110
|
+
if (!this.sessionStarted)
|
|
111
|
+
return;
|
|
112
|
+
this.sessionStarted = false;
|
|
113
|
+
await this.hooks.run({
|
|
114
|
+
event: "SessionEnd",
|
|
115
|
+
sessionId: this.session.id,
|
|
116
|
+
agentId: this.agentId,
|
|
117
|
+
cwd: this.session.cwd,
|
|
118
|
+
messages: this.session.messages,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Cancel the in-flight turn. The loop exits with `stopReason: "aborted"`. */
|
|
122
|
+
abort() {
|
|
123
|
+
this.controller.abort();
|
|
124
|
+
}
|
|
125
|
+
get isRunning() {
|
|
126
|
+
return this.running;
|
|
127
|
+
}
|
|
128
|
+
// --- ToolHost -----------------------------------------------------------
|
|
129
|
+
/**
|
|
130
|
+
* Where the file tools are allowed to look. The policy lives here, so the
|
|
131
|
+
* tools ask rather than each re-deriving a notion of "inside the project".
|
|
132
|
+
*/
|
|
133
|
+
resolvePath(input, access) {
|
|
134
|
+
return resolveWithin(this.opts.workspace, this.session.cwd, input, access);
|
|
135
|
+
}
|
|
136
|
+
listSubagentTypes() {
|
|
137
|
+
return this.subagents.summaries();
|
|
138
|
+
}
|
|
139
|
+
async runSubagent(opts) {
|
|
140
|
+
const definition = this.subagents.get(opts.agentType);
|
|
141
|
+
if (!definition)
|
|
142
|
+
throw new Error(`unknown agent type: ${opts.agentType}`);
|
|
143
|
+
// A subagent gets its own transcript and a filtered tool set, but shares
|
|
144
|
+
// the parent's scratch state so file-read tracking stays coherent.
|
|
145
|
+
const { Session: SessionClass } = await import("./session.js");
|
|
146
|
+
const child = new SessionClass({
|
|
147
|
+
cwd: this.session.cwd,
|
|
148
|
+
model: opts.model ?? definition.model ?? this.session.model.id,
|
|
149
|
+
providers: this.session.providers,
|
|
150
|
+
permissionMode: this.session.permissions.getMode(),
|
|
151
|
+
store: false,
|
|
152
|
+
});
|
|
153
|
+
for (const [key, value] of this.session.state)
|
|
154
|
+
child.state.set(key, value);
|
|
155
|
+
const childAgent = new Agent({
|
|
156
|
+
...this.opts,
|
|
157
|
+
session: child,
|
|
158
|
+
tools: definition.tools ? this.tools.filtered(definition.tools) : this.tools.clone(),
|
|
159
|
+
systemPrompt: definition.systemPrompt,
|
|
160
|
+
appendSystemPrompt: undefined,
|
|
161
|
+
maxTurns: definition.maxTurns ?? this.opts.maxTurns,
|
|
162
|
+
agentId: `${this.agentId}/${definition.name}`,
|
|
163
|
+
// One level of delegation: a subagent may not spawn further subagents.
|
|
164
|
+
subagents: new SubagentRegistry([]),
|
|
165
|
+
});
|
|
166
|
+
let text = "";
|
|
167
|
+
let turns = 0;
|
|
168
|
+
let usage = emptyUsage();
|
|
169
|
+
for await (const event of childAgent.run(opts.prompt, { signal: opts.signal })) {
|
|
170
|
+
if (event.type === "assistant_message") {
|
|
171
|
+
const messageBody = messageText(event.message);
|
|
172
|
+
if (messageBody.trim())
|
|
173
|
+
text = messageBody;
|
|
174
|
+
}
|
|
175
|
+
else if (event.type === "done") {
|
|
176
|
+
turns = event.turns;
|
|
177
|
+
usage = event.usage;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Propagate state the subagent built up (e.g. files it read).
|
|
181
|
+
for (const [key, value] of child.state)
|
|
182
|
+
this.session.state.set(key, value);
|
|
183
|
+
await this.hooks.run({
|
|
184
|
+
event: "SubagentStop",
|
|
185
|
+
sessionId: this.session.id,
|
|
186
|
+
agentId: `${this.agentId}/${definition.name}`,
|
|
187
|
+
cwd: this.session.cwd,
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
text,
|
|
191
|
+
turns,
|
|
192
|
+
usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// --- Main loop ----------------------------------------------------------
|
|
196
|
+
async *run(input, runOpts = {}) {
|
|
197
|
+
this.running = true;
|
|
198
|
+
this.controller = new AbortController();
|
|
199
|
+
const external = runOpts.signal;
|
|
200
|
+
if (external) {
|
|
201
|
+
if (external.aborted)
|
|
202
|
+
this.controller.abort();
|
|
203
|
+
else
|
|
204
|
+
external.addEventListener("abort", () => this.controller.abort(), { once: true });
|
|
205
|
+
}
|
|
206
|
+
const signal = this.controller.signal;
|
|
207
|
+
// Fired once per session rather than per turn: hooks use it to seed
|
|
208
|
+
// context or announce the session, which should not repeat every prompt.
|
|
209
|
+
if (!this.sessionStarted) {
|
|
210
|
+
this.sessionStarted = true;
|
|
211
|
+
const start = await this.hooks.run({
|
|
212
|
+
event: "SessionStart",
|
|
213
|
+
sessionId: this.session.id,
|
|
214
|
+
agentId: this.agentId,
|
|
215
|
+
cwd: this.session.cwd,
|
|
216
|
+
messages: this.session.messages,
|
|
217
|
+
});
|
|
218
|
+
if (start.additionalContext) {
|
|
219
|
+
yield { type: "notice", message: start.additionalContext };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const maxTurns = this.opts.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
223
|
+
let stopReason = "end_turn";
|
|
224
|
+
let turn = 0;
|
|
225
|
+
try {
|
|
226
|
+
await this.session.init();
|
|
227
|
+
// Read once per run: the index is only rebuilt when the agent writes a
|
|
228
|
+
// note, and re-reading the directory every turn would be wasteful.
|
|
229
|
+
this.memoryIndex = await this.memory?.promptSection();
|
|
230
|
+
yield* this.ingestInput(input);
|
|
231
|
+
while (turn < maxTurns) {
|
|
232
|
+
if (signal.aborted) {
|
|
233
|
+
stopReason = "aborted";
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
turn++;
|
|
237
|
+
this.session.turns++;
|
|
238
|
+
yield { type: "turn_start", turn, model: this.session.model.id };
|
|
239
|
+
yield* this.maybeCompact();
|
|
240
|
+
const request = this.buildRequest(signal);
|
|
241
|
+
await this.hooks.run({
|
|
242
|
+
event: "PreModelRequest",
|
|
243
|
+
sessionId: this.session.id,
|
|
244
|
+
agentId: this.agentId,
|
|
245
|
+
cwd: this.session.cwd,
|
|
246
|
+
messages: this.session.messages,
|
|
247
|
+
});
|
|
248
|
+
let assembled;
|
|
249
|
+
try {
|
|
250
|
+
assembled = yield* this.streamAssistant(request);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
const message = errorMessage(err);
|
|
254
|
+
this.logger.error("provider request failed", { error: message });
|
|
255
|
+
// Read off the error rather than narrowed to ProviderError: the
|
|
256
|
+
// engine is imported through more than one specifier in this
|
|
257
|
+
// workspace, and an instanceof against a class that may have been
|
|
258
|
+
// loaded twice is a check that fails for reasons unrelated to what
|
|
259
|
+
// it is asking.
|
|
260
|
+
const status = err?.status;
|
|
261
|
+
// And which vendor said it, read off the same way and for the same
|
|
262
|
+
// reason the status is: a 401 is not a sentence to print but a
|
|
263
|
+
// provider to reconnect, and the consumer cannot know which one that
|
|
264
|
+
// is — a fallback may have moved off the model the session names.
|
|
265
|
+
const provider = err?.provider;
|
|
266
|
+
yield {
|
|
267
|
+
type: "error",
|
|
268
|
+
error: message,
|
|
269
|
+
...(typeof status === "number" ? { status } : {}),
|
|
270
|
+
...(typeof provider === "string" ? { provider } : {}),
|
|
271
|
+
fatal: true,
|
|
272
|
+
};
|
|
273
|
+
stopReason = "error";
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
this.session.recordUsage(assembled.usage);
|
|
277
|
+
const message = assistantMessage(assembled.content, {
|
|
278
|
+
model: this.session.model.model,
|
|
279
|
+
provider: this.session.model.provider,
|
|
280
|
+
usage: assembled.usage,
|
|
281
|
+
stopReason: assembled.stopReason,
|
|
282
|
+
});
|
|
283
|
+
// An empty assistant turn would be rejected on the next request, so
|
|
284
|
+
// never persist one.
|
|
285
|
+
if (message.content.length > 0) {
|
|
286
|
+
await this.session.addMessage(message);
|
|
287
|
+
yield { type: "assistant_message", message };
|
|
288
|
+
}
|
|
289
|
+
const calls = message.content.filter((block) => block.type === "tool_call");
|
|
290
|
+
if (calls.length === 0) {
|
|
291
|
+
stopReason = assembled.stopReason === "tool_use" ? "end_turn" : assembled.stopReason;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
const results = [];
|
|
295
|
+
for await (const event of this.executeCalls(calls, signal, results)) {
|
|
296
|
+
yield event;
|
|
297
|
+
}
|
|
298
|
+
// All tool results for a turn must go back in one user message —
|
|
299
|
+
// splitting them teaches the model to stop calling tools in parallel.
|
|
300
|
+
await this.session.addMessage({
|
|
301
|
+
role: "user",
|
|
302
|
+
content: results,
|
|
303
|
+
meta: { timestamp: Date.now(), synthetic: true },
|
|
304
|
+
});
|
|
305
|
+
if (this.stopRequested) {
|
|
306
|
+
yield { type: "notice", message: this.stopRequested };
|
|
307
|
+
this.stopRequested = undefined;
|
|
308
|
+
stopReason = "end_turn";
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (signal.aborted) {
|
|
312
|
+
stopReason = "aborted";
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (turn >= maxTurns && stopReason === "end_turn") {
|
|
317
|
+
yield {
|
|
318
|
+
type: "notice",
|
|
319
|
+
message: `Stopped after ${maxTurns} turns without finishing. Send another message to continue.`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
await this.hooks.run({
|
|
323
|
+
event: "Stop",
|
|
324
|
+
sessionId: this.session.id,
|
|
325
|
+
agentId: this.agentId,
|
|
326
|
+
cwd: this.session.cwd,
|
|
327
|
+
messages: this.session.messages,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
this.running = false;
|
|
332
|
+
await this.session.persist().catch(() => { });
|
|
333
|
+
}
|
|
334
|
+
yield {
|
|
335
|
+
type: "done",
|
|
336
|
+
stopReason,
|
|
337
|
+
usage: this.session.usage,
|
|
338
|
+
costUsd: this.session.costUsd,
|
|
339
|
+
turns: turn,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// --- Steps --------------------------------------------------------------
|
|
343
|
+
async *ingestInput(input) {
|
|
344
|
+
if (Array.isArray(input)) {
|
|
345
|
+
for (const message of input)
|
|
346
|
+
await this.session.addMessage(message);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const outcome = await this.hooks.run({
|
|
350
|
+
event: "UserPromptSubmit",
|
|
351
|
+
sessionId: this.session.id,
|
|
352
|
+
agentId: this.agentId,
|
|
353
|
+
cwd: this.session.cwd,
|
|
354
|
+
prompt: input,
|
|
355
|
+
});
|
|
356
|
+
if (outcome.decision === "deny") {
|
|
357
|
+
yield { type: "error", error: outcome.message ?? "prompt rejected by hook", fatal: true };
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const text = outcome.additionalContext
|
|
361
|
+
? `${input}\n\n<hook-context>\n${outcome.additionalContext}\n</hook-context>`
|
|
362
|
+
: input;
|
|
363
|
+
await this.session.addMessage(userMessage(text));
|
|
364
|
+
}
|
|
365
|
+
async *maybeCompact() {
|
|
366
|
+
if (this.opts.compaction === false)
|
|
367
|
+
return;
|
|
368
|
+
const systemTokens = estimateStringTokens(this.systemPrompt());
|
|
369
|
+
if (!shouldCompact(this.session.messages, this.session.model, systemTokens, this.opts.compaction)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
await this.hooks.run({
|
|
373
|
+
event: "PreCompact",
|
|
374
|
+
sessionId: this.session.id,
|
|
375
|
+
agentId: this.agentId,
|
|
376
|
+
cwd: this.session.cwd,
|
|
377
|
+
messages: this.session.messages,
|
|
378
|
+
});
|
|
379
|
+
try {
|
|
380
|
+
// Summarizing is a cheap, well-specified task — run it on the small
|
|
381
|
+
// model when one is configured rather than on whatever expensive model
|
|
382
|
+
// the session happens to be using.
|
|
383
|
+
const summarizer = this.summarizerModel();
|
|
384
|
+
const provider = this.session.providers.getProvider(summarizer.provider);
|
|
385
|
+
const result = await compact(this.session.messages, provider, summarizer, this.opts.compaction ?? {});
|
|
386
|
+
if (result.droppedMessages === 0)
|
|
387
|
+
return;
|
|
388
|
+
// compactTo archives what it drops, so nothing becomes unrecoverable.
|
|
389
|
+
await this.session.compactTo(result.messages, result.dropped);
|
|
390
|
+
yield {
|
|
391
|
+
type: "compacted",
|
|
392
|
+
tokensBefore: result.tokensBefore,
|
|
393
|
+
tokensAfter: result.tokensAfter,
|
|
394
|
+
droppedMessages: result.droppedMessages,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
// Failing to compact is not fatal — the request may still fit.
|
|
399
|
+
this.logger.warn("compaction failed", { error: errorMessage(err) });
|
|
400
|
+
yield { type: "notice", message: `Compaction failed: ${errorMessage(err)}` };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
systemPrompt() {
|
|
404
|
+
return buildSystemPrompt({
|
|
405
|
+
systemPrompt: this.opts.systemPrompt,
|
|
406
|
+
appendSystemPrompt: this.opts.appendSystemPrompt,
|
|
407
|
+
cwd: this.session.cwd,
|
|
408
|
+
model: this.session.model,
|
|
409
|
+
permissionMode: this.session.permissions.getMode(),
|
|
410
|
+
projectInstructions: this.opts.projectInstructions,
|
|
411
|
+
mcpInstructions: this.opts.mcp?.serverInstructions(),
|
|
412
|
+
toolNames: this.tools.visible(this.opts.toolFilter).map((t) => t.name),
|
|
413
|
+
// Only the descriptions — loading every skill body up front would
|
|
414
|
+
// defeat the point of having them be loadable.
|
|
415
|
+
extraSections: [this.skills.promptSection(), this.memoryIndex].filter((section) => section !== undefined),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
buildRequest(signal) {
|
|
419
|
+
const model = this.session.model;
|
|
420
|
+
return {
|
|
421
|
+
model,
|
|
422
|
+
system: this.systemPrompt(),
|
|
423
|
+
messages: this.session.messages,
|
|
424
|
+
tools: model.supportsTools ? this.tools.schemas(this.opts.toolFilter) : undefined,
|
|
425
|
+
toolChoice: { type: "auto" },
|
|
426
|
+
thinking: this.opts.thinking,
|
|
427
|
+
cache: this.opts.cache ?? true,
|
|
428
|
+
signal,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/** Consume the provider stream, emitting deltas and assembling the message. */
|
|
432
|
+
async *streamAssistant(request) {
|
|
433
|
+
const provider = this.session.providers.getProvider(request.model.provider);
|
|
434
|
+
let text = "";
|
|
435
|
+
let thinking = "";
|
|
436
|
+
let signature;
|
|
437
|
+
const toolCalls = [];
|
|
438
|
+
let usage = emptyUsage();
|
|
439
|
+
let stopReason = "end_turn";
|
|
440
|
+
for await (const event of provider.stream(request)) {
|
|
441
|
+
switch (event.type) {
|
|
442
|
+
case "text_delta":
|
|
443
|
+
text += event.text;
|
|
444
|
+
yield { type: "text_delta", text: event.text };
|
|
445
|
+
break;
|
|
446
|
+
case "thinking_delta":
|
|
447
|
+
thinking += event.text;
|
|
448
|
+
yield { type: "thinking_delta", text: event.text };
|
|
449
|
+
break;
|
|
450
|
+
case "thinking_signature":
|
|
451
|
+
signature = event.signature;
|
|
452
|
+
break;
|
|
453
|
+
case "tool_call_end":
|
|
454
|
+
toolCalls.push({
|
|
455
|
+
type: "tool_call",
|
|
456
|
+
id: event.id,
|
|
457
|
+
name: event.name,
|
|
458
|
+
input: event.input,
|
|
459
|
+
});
|
|
460
|
+
break;
|
|
461
|
+
case "usage":
|
|
462
|
+
usage = event.usage;
|
|
463
|
+
break;
|
|
464
|
+
case "end":
|
|
465
|
+
stopReason = event.stopReason;
|
|
466
|
+
usage = event.usage;
|
|
467
|
+
break;
|
|
468
|
+
default:
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Anthropic requires thinking blocks first; the others don't care, so a
|
|
473
|
+
// single canonical order works everywhere.
|
|
474
|
+
const content = [];
|
|
475
|
+
if (thinking) {
|
|
476
|
+
const block = { type: "thinking", thinking };
|
|
477
|
+
if (signature)
|
|
478
|
+
block.signature = signature;
|
|
479
|
+
content.push(block);
|
|
480
|
+
}
|
|
481
|
+
if (text)
|
|
482
|
+
content.push({ type: "text", text });
|
|
483
|
+
content.push(...toolCalls);
|
|
484
|
+
return { content, usage, stopReason };
|
|
485
|
+
}
|
|
486
|
+
async *executeCalls(calls, signal, results) {
|
|
487
|
+
// Parallelize only when every call in the turn is safe to run
|
|
488
|
+
// concurrently — one unsafe call forces the whole batch to serialize,
|
|
489
|
+
// because we cannot know how they interact.
|
|
490
|
+
const parallel = calls.length > 1 &&
|
|
491
|
+
calls.every((call) => this.tools.get(call.name)?.concurrencySafe === true);
|
|
492
|
+
if (!parallel) {
|
|
493
|
+
for (const call of calls) {
|
|
494
|
+
yield* this.executeOne(call, signal, results);
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
// Merge the per-call event streams so the UI sees tools interleave as they
|
|
499
|
+
// actually run. Buffering until every call finished made parallel
|
|
500
|
+
// execution indistinguishable from serial.
|
|
501
|
+
const collected = new Map();
|
|
502
|
+
yield* mergeStreams(calls.map((call) => async function* () {
|
|
503
|
+
const local = [];
|
|
504
|
+
yield* this.executeOne(call, signal, local);
|
|
505
|
+
if (local[0])
|
|
506
|
+
collected.set(call.id, local[0]);
|
|
507
|
+
}.bind(this)));
|
|
508
|
+
// Results still go back in the model's original call order, independent of
|
|
509
|
+
// which finished first.
|
|
510
|
+
for (const call of calls) {
|
|
511
|
+
const result = collected.get(call.id);
|
|
512
|
+
if (result)
|
|
513
|
+
results.push(result);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async *executeOne(call, signal, results) {
|
|
517
|
+
const started = Date.now();
|
|
518
|
+
const tool = this.tools.get(call.name);
|
|
519
|
+
if (!tool) {
|
|
520
|
+
const available = this.tools.visible(this.opts.toolFilter).map((t) => t.name).join(", ");
|
|
521
|
+
results.push(errorResult(call, `No tool named "${call.name}". Available tools: ${available}`));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
// Validate before anything else so a malformed call costs nothing.
|
|
525
|
+
let input = call.input;
|
|
526
|
+
if (tool.validate) {
|
|
527
|
+
const parsed = tool.validate(input);
|
|
528
|
+
if (!parsed.ok) {
|
|
529
|
+
results.push(errorResult(call, `Invalid input for ${call.name}: ${parsed.error}`));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
input = parsed.value;
|
|
533
|
+
}
|
|
534
|
+
const title = tool.renderTitle?.(input) ?? `${call.name}(${previewJson(input, 60)})`;
|
|
535
|
+
yield { type: "tool_start", toolCallId: call.id, toolName: call.name, title, input };
|
|
536
|
+
const pre = await this.hooks.run({
|
|
537
|
+
event: "PreToolUse",
|
|
538
|
+
sessionId: this.session.id,
|
|
539
|
+
agentId: this.agentId,
|
|
540
|
+
cwd: this.session.cwd,
|
|
541
|
+
toolName: call.name,
|
|
542
|
+
toolInput: input,
|
|
543
|
+
});
|
|
544
|
+
if (pre.decision === "deny") {
|
|
545
|
+
const reason = pre.message ?? "blocked by a PreToolUse hook";
|
|
546
|
+
yield { type: "permission_result", toolCallId: call.id, allowed: false, reason };
|
|
547
|
+
results.push(errorResult(call, reason));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (pre.updatedInput !== undefined)
|
|
551
|
+
input = pre.updatedInput;
|
|
552
|
+
if (pre.stop) {
|
|
553
|
+
// A hook asking to stop is stronger than denying one tool: end the turn
|
|
554
|
+
// rather than handing the model an error it will try to work around.
|
|
555
|
+
const reason = pre.message ?? "stopped by a PreToolUse hook";
|
|
556
|
+
results.push(errorResult(call, reason));
|
|
557
|
+
this.stopRequested = reason;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
// The workspace policy runs before the approval prompt: a write the
|
|
561
|
+
// policy forbids should never be offered for approval at all.
|
|
562
|
+
if (this.opts.workspace && !tool.readOnly) {
|
|
563
|
+
const check = checkWrite(this.opts.workspace, call.name, input, this.session.cwd);
|
|
564
|
+
if (!check.allowed) {
|
|
565
|
+
const reason = check.reason ?? "blocked by the workspace policy";
|
|
566
|
+
yield { type: "permission_result", toolCallId: call.id, allowed: false, reason };
|
|
567
|
+
results.push(errorResult(call, reason));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const allowed = yield* this.checkPermission(call, tool, input, title);
|
|
572
|
+
if (!allowed)
|
|
573
|
+
return void results.push(errorResult(call, permissionDenialText(title)));
|
|
574
|
+
const ctx = {
|
|
575
|
+
cwd: this.session.cwd,
|
|
576
|
+
sessionId: this.session.id,
|
|
577
|
+
agentId: this.agentId,
|
|
578
|
+
toolCallId: call.id,
|
|
579
|
+
signal,
|
|
580
|
+
logger: this.logger.child(call.name),
|
|
581
|
+
onProgress: () => { },
|
|
582
|
+
host: this,
|
|
583
|
+
};
|
|
584
|
+
// Progress has to reach the consumer while the tool is still running —
|
|
585
|
+
// buffering it until completion turns a long-running tool into a frozen
|
|
586
|
+
// dot on screen.
|
|
587
|
+
const progress = new AsyncQueue();
|
|
588
|
+
ctx.onProgress = (update) => progress.push({ type: "tool_progress", progress: update });
|
|
589
|
+
let result;
|
|
590
|
+
const execution = (async () => {
|
|
591
|
+
try {
|
|
592
|
+
result = await tool.execute(input, ctx);
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
result = {
|
|
596
|
+
content: [{ type: "text", text: `Tool threw: ${errorMessage(err)}` }],
|
|
597
|
+
isError: true,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
progress.close();
|
|
602
|
+
}
|
|
603
|
+
})();
|
|
604
|
+
yield* progress;
|
|
605
|
+
await execution;
|
|
606
|
+
// The finally block above always assigns, so this is defensive only.
|
|
607
|
+
result ??= { content: [{ type: "text", text: "Tool produced no result" }], isError: true };
|
|
608
|
+
const post = await this.hooks.run({
|
|
609
|
+
event: "PostToolUse",
|
|
610
|
+
sessionId: this.session.id,
|
|
611
|
+
agentId: this.agentId,
|
|
612
|
+
cwd: this.session.cwd,
|
|
613
|
+
toolName: call.name,
|
|
614
|
+
toolInput: input,
|
|
615
|
+
result,
|
|
616
|
+
});
|
|
617
|
+
if (post.updatedResult)
|
|
618
|
+
result = post.updatedResult;
|
|
619
|
+
// Cap the result last, after hooks have had their say. A tool result lands
|
|
620
|
+
// in the transcript after this turn's compaction check has already run, so
|
|
621
|
+
// an uncapped result is the one thing compaction cannot protect against.
|
|
622
|
+
const capped = capToolResult(result, {
|
|
623
|
+
toolName: call.name,
|
|
624
|
+
toolCallId: call.id,
|
|
625
|
+
limit: this.opts.toolResultLimit,
|
|
626
|
+
});
|
|
627
|
+
if (capped.archived) {
|
|
628
|
+
await this.session.archiveOverflow(capped.archived);
|
|
629
|
+
this.logger.debug("archived oversized tool output", {
|
|
630
|
+
tool: call.name,
|
|
631
|
+
chars: capped.originalChars,
|
|
632
|
+
});
|
|
633
|
+
yield {
|
|
634
|
+
type: "notice",
|
|
635
|
+
message: `${call.name} returned ${capped.originalChars.toLocaleString()} characters; truncated and archived (recall to search it).`,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
result = capped.result;
|
|
639
|
+
// A write into a skills directory is the agent authoring a skill for
|
|
640
|
+
// itself. Re-registering here is what closes the loop — otherwise the
|
|
641
|
+
// skill sits on disk unusable until the process restarts.
|
|
642
|
+
if (!result.isError && this.opts.onSkillsChanged && touchesSkills(call.name, input)) {
|
|
643
|
+
await this.opts.onSkillsChanged();
|
|
644
|
+
if (this.skills.size > 0 && !this.tools.has("skill")) {
|
|
645
|
+
this.tools.register(createSkillTool(this.skills));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const durationMs = Date.now() - started;
|
|
649
|
+
yield { type: "tool_end", toolCallId: call.id, toolName: call.name, result, durationMs };
|
|
650
|
+
// Read off the final result rather than captured at execute time, so a
|
|
651
|
+
// PostToolUse hook that rewrites or suppresses the result suppresses the
|
|
652
|
+
// artifact with it. The descriptor survives `capToolResult`, which spreads
|
|
653
|
+
// the metadata it was given.
|
|
654
|
+
const produced = result.metadata?.[ARTIFACT_METADATA_KEY];
|
|
655
|
+
if (!result.isError && isArtifactDescriptor(produced)) {
|
|
656
|
+
yield { type: "artifact", artifact: produced };
|
|
657
|
+
}
|
|
658
|
+
results.push({
|
|
659
|
+
type: "tool_result",
|
|
660
|
+
toolCallId: call.id,
|
|
661
|
+
toolName: call.name,
|
|
662
|
+
content: result.content,
|
|
663
|
+
isError: result.isError,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
async *checkPermission(call, tool, input, title) {
|
|
667
|
+
const decision = this.session.permissions.evaluate(tool, input);
|
|
668
|
+
if (decision.behavior === "allow")
|
|
669
|
+
return true;
|
|
670
|
+
if (decision.behavior === "deny") {
|
|
671
|
+
yield {
|
|
672
|
+
type: "permission_result",
|
|
673
|
+
toolCallId: call.id,
|
|
674
|
+
allowed: false,
|
|
675
|
+
reason: decision.message,
|
|
676
|
+
};
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
const request = {
|
|
680
|
+
toolName: call.name,
|
|
681
|
+
input,
|
|
682
|
+
title,
|
|
683
|
+
reason: decision.rule ? `matched rule ${decision.rule}` : undefined,
|
|
684
|
+
};
|
|
685
|
+
yield { type: "permission_request", request };
|
|
686
|
+
// With no way to ask, an `ask` decision has to fail closed. Recorded as
|
|
687
|
+
// its own outcome rather than as a refusal: nobody refused this, nobody
|
|
688
|
+
// ever saw it, and a trail that cannot tell those apart is misleading in
|
|
689
|
+
// exactly the case somebody is reading it to understand.
|
|
690
|
+
if (!this.opts.permissionPrompt) {
|
|
691
|
+
this.session.permissions.recordAnswer(decision.auditId, "unavailable");
|
|
692
|
+
yield {
|
|
693
|
+
type: "permission_result",
|
|
694
|
+
toolCallId: call.id,
|
|
695
|
+
allowed: false,
|
|
696
|
+
reason: "no approval handler is configured",
|
|
697
|
+
};
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
const answer = await this.opts.permissionPrompt(request);
|
|
701
|
+
const allowed = typeof answer === "boolean" ? answer : answer.allow;
|
|
702
|
+
let remembered;
|
|
703
|
+
if (allowed && typeof answer === "object" && answer.remember) {
|
|
704
|
+
// `true` keeps the old session-only behaviour; a scope name persists.
|
|
705
|
+
const scope = answer.remember === true ? "session" : answer.remember;
|
|
706
|
+
remembered = await this.session.permissions.rememberAllow(tool, input, scope);
|
|
707
|
+
}
|
|
708
|
+
this.session.permissions.recordAnswer(decision.auditId, allowed ? "allowed" : "denied", remembered);
|
|
709
|
+
yield {
|
|
710
|
+
type: "permission_result",
|
|
711
|
+
toolCallId: call.id,
|
|
712
|
+
allowed,
|
|
713
|
+
...(remembered ? { remembered } : {}),
|
|
714
|
+
};
|
|
715
|
+
return allowed;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Metadata is `Record<string, unknown>` and reaches here through a hook that
|
|
720
|
+
* any user can write, so the descriptor is checked rather than asserted. A
|
|
721
|
+
* malformed one is dropped: a frontend that renders an artifact row from a
|
|
722
|
+
* half-built object is worse than one that never hears about it.
|
|
723
|
+
*/
|
|
724
|
+
function isArtifactDescriptor(value) {
|
|
725
|
+
if (typeof value !== "object" || value === null)
|
|
726
|
+
return false;
|
|
727
|
+
const d = value;
|
|
728
|
+
return (typeof d["id"] === "string" &&
|
|
729
|
+
typeof d["title"] === "string" &&
|
|
730
|
+
typeof d["mime"] === "string" &&
|
|
731
|
+
typeof d["kind"] === "string" &&
|
|
732
|
+
typeof d["version"] === "number" &&
|
|
733
|
+
typeof d["bytes"] === "number");
|
|
734
|
+
}
|
|
735
|
+
function errorResult(call, text) {
|
|
736
|
+
return {
|
|
737
|
+
type: "tool_result",
|
|
738
|
+
toolCallId: call.id,
|
|
739
|
+
toolName: call.name,
|
|
740
|
+
content: [{ type: "text", text }],
|
|
741
|
+
isError: true,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function permissionDenialText(title) {
|
|
745
|
+
return `The user did not approve ${title}. Do not retry it. Explain what you wanted to do and ask how to proceed, or continue with another approach.`;
|
|
746
|
+
}
|
|
747
|
+
/** Whether a tool call wrote something that looks like a skill definition. */
|
|
748
|
+
function touchesSkills(toolName, input) {
|
|
749
|
+
if (toolName !== "write" && toolName !== "edit")
|
|
750
|
+
return false;
|
|
751
|
+
const target = input?.path;
|
|
752
|
+
if (typeof target !== "string")
|
|
753
|
+
return false;
|
|
754
|
+
// Only SKILL.md matters: the supporting scripts a skill ships do not change
|
|
755
|
+
// what is advertised, so re-scanning for those would be noise.
|
|
756
|
+
return target.includes("skills") && target.endsWith("SKILL.md");
|
|
757
|
+
}
|
|
758
|
+
//# sourceMappingURL=agent.js.map
|