@iinm/plain-agent 1.8.2 → 1.8.4
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 +6 -2
- package/bin/plain +1 -1
- package/config/config.predefined.json +1 -1
- package/config/prompts.predefined/shortcuts/configure.md +1 -1
- package/dist/main.mjs +473 -0
- package/dist/main.mjs.map +7 -0
- package/package.json +5 -7
- package/src/agent.d.ts +0 -52
- package/src/agent.mjs +0 -204
- package/src/agentLoop.mjs +0 -419
- package/src/agentState.mjs +0 -41
- package/src/claudeCodePlugin.mjs +0 -164
- package/src/cliArgs.mjs +0 -175
- package/src/cliBatch.mjs +0 -144
- package/src/cliCommands.mjs +0 -283
- package/src/cliCompleter.mjs +0 -227
- package/src/cliCost.mjs +0 -309
- package/src/cliFormatter.mjs +0 -413
- package/src/cliInteractive.mjs +0 -526
- package/src/cliInterruptTransform.mjs +0 -51
- package/src/cliMuteTransform.mjs +0 -26
- package/src/cliPasteTransform.mjs +0 -183
- package/src/config.d.ts +0 -36
- package/src/config.mjs +0 -197
- package/src/context/loadAgentRoles.mjs +0 -283
- package/src/context/loadPrompts.mjs +0 -324
- package/src/context/loadUserMessageContext.mjs +0 -147
- package/src/costTracker.mjs +0 -210
- package/src/env.mjs +0 -44
- package/src/main.mjs +0 -278
- package/src/mcpClient.mjs +0 -351
- package/src/mcpIntegration.mjs +0 -160
- package/src/model.d.ts +0 -109
- package/src/modelCaller.mjs +0 -32
- package/src/modelDefinition.d.ts +0 -92
- package/src/prompt.mjs +0 -138
- package/src/providers/anthropic.d.ts +0 -248
- package/src/providers/anthropic.mjs +0 -587
- package/src/providers/bedrock.d.ts +0 -249
- package/src/providers/bedrock.mjs +0 -700
- package/src/providers/gemini.d.ts +0 -208
- package/src/providers/gemini.mjs +0 -754
- package/src/providers/openai.d.ts +0 -281
- package/src/providers/openai.mjs +0 -544
- package/src/providers/openaiCompatible.d.ts +0 -147
- package/src/providers/openaiCompatible.mjs +0 -652
- package/src/providers/platform/awsSigV4.mjs +0 -184
- package/src/providers/platform/azure.mjs +0 -42
- package/src/providers/platform/bedrock.mjs +0 -78
- package/src/providers/platform/googleCloud.mjs +0 -34
- package/src/subagent.mjs +0 -265
- package/src/tmpfile.mjs +0 -27
- package/src/tool.d.ts +0 -74
- package/src/toolExecutor.mjs +0 -236
- package/src/toolInputValidator.mjs +0 -183
- package/src/toolUseApprover.mjs +0 -99
- package/src/tools/askURL.mjs +0 -209
- package/src/tools/askWeb.mjs +0 -208
- package/src/tools/compactContext.d.ts +0 -4
- package/src/tools/compactContext.mjs +0 -87
- package/src/tools/delegateToSubagent.d.ts +0 -4
- package/src/tools/delegateToSubagent.mjs +0 -48
- package/src/tools/execCommand.d.ts +0 -22
- package/src/tools/execCommand.mjs +0 -200
- package/src/tools/patchFile.d.ts +0 -4
- package/src/tools/patchFile.mjs +0 -133
- package/src/tools/reportAsSubagent.d.ts +0 -3
- package/src/tools/reportAsSubagent.mjs +0 -44
- package/src/tools/tmuxCommand.d.ts +0 -14
- package/src/tools/tmuxCommand.mjs +0 -194
- package/src/tools/writeFile.d.ts +0 -4
- package/src/tools/writeFile.mjs +0 -56
- package/src/usageStore.mjs +0 -167
- package/src/utils/evalJSONConfig.mjs +0 -72
- package/src/utils/matchValue.d.ts +0 -6
- package/src/utils/matchValue.mjs +0 -40
- package/src/utils/noThrow.mjs +0 -31
- package/src/utils/notify.mjs +0 -29
- package/src/utils/parseFileRange.mjs +0 -18
- package/src/utils/readFileRange.mjs +0 -33
- package/src/utils/retryOnError.mjs +0 -41
- package/src/voiceInput.mjs +0 -61
- package/src/voiceInputGemini.mjs +0 -105
- package/src/voiceInputOpenAI.mjs +0 -104
- package/src/voiceInputSession.mjs +0 -543
- package/src/voiceToggleKey.mjs +0 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iinm/plain-agent",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
4
4
|
"description": "A lightweight CLI-based coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,17 +17,14 @@
|
|
|
17
17
|
"bin",
|
|
18
18
|
"sandbox/bin",
|
|
19
19
|
"config",
|
|
20
|
-
"
|
|
21
|
-
"src/**/*.d.ts",
|
|
22
|
-
"!src/**/*.test.mjs",
|
|
23
|
-
"!src/**/*.test.*.mjs",
|
|
24
|
-
"!src/**/*.playground.mjs",
|
|
25
|
-
"!src/**/test/"
|
|
20
|
+
"dist"
|
|
26
21
|
],
|
|
27
22
|
"engines": {
|
|
28
23
|
"node": ">=22"
|
|
29
24
|
},
|
|
30
25
|
"scripts": {
|
|
26
|
+
"build": "esbuild src/main.mjs --bundle --platform=node --target=node22 --outfile=dist/main.mjs --minify --sourcemap --format=esm --banner:js='import { createRequire } from \"node:module\";const require=createRequire(import.meta.url);'",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
31
28
|
"check": "npm run lint && tsc && npm run test",
|
|
32
29
|
"test": "node --test",
|
|
33
30
|
"lint": "npx @biomejs/biome check",
|
|
@@ -40,6 +37,7 @@
|
|
|
40
37
|
"devDependencies": {
|
|
41
38
|
"@biomejs/biome": "^2.4.12",
|
|
42
39
|
"@types/node": "^22.19.17",
|
|
40
|
+
"esbuild": "^0.28.0",
|
|
43
41
|
"typescript": "^6.0.2"
|
|
44
42
|
}
|
|
45
43
|
}
|
package/src/agent.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { EventEmitter } from "node:events";
|
|
2
|
-
import type { AgentRole } from "./context/loadAgentRoles.mjs";
|
|
3
|
-
import type { CostConfig, CostSummary } from "./costTracker.mjs";
|
|
4
|
-
import type {
|
|
5
|
-
CallModel,
|
|
6
|
-
Message,
|
|
7
|
-
MessageContentImage,
|
|
8
|
-
MessageContentText,
|
|
9
|
-
PartialMessageContent,
|
|
10
|
-
ProviderTokenUsage,
|
|
11
|
-
} from "./model";
|
|
12
|
-
import type { Tool, ToolUseApprover } from "./tool";
|
|
13
|
-
|
|
14
|
-
export type Agent = {
|
|
15
|
-
userEventEmitter: UserEventEmitter;
|
|
16
|
-
agentEventEmitter: AgentEventEmitter;
|
|
17
|
-
agentCommands: AgentCommands;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type AgentCommands = {
|
|
21
|
-
dumpMessages: () => Promise<void>;
|
|
22
|
-
loadMessages: () => Promise<void>;
|
|
23
|
-
getCostSummary: () => CostSummary;
|
|
24
|
-
pauseAutoApprove: () => void;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type UserEventMap = {
|
|
28
|
-
userInput: [(MessageContentText | MessageContentImage)[]];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type UserEventEmitter = EventEmitter<UserEventMap>;
|
|
32
|
-
|
|
33
|
-
type AgentEventMap = {
|
|
34
|
-
message: [Message];
|
|
35
|
-
partialMessageContent: [PartialMessageContent];
|
|
36
|
-
error: [Error];
|
|
37
|
-
toolUseRequest: [];
|
|
38
|
-
turnEnd: [];
|
|
39
|
-
providerTokenUsage: [ProviderTokenUsage];
|
|
40
|
-
subagentSwitched: [{ name: string } | null];
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export type AgentEventEmitter = EventEmitter<AgentEventMap>;
|
|
44
|
-
|
|
45
|
-
export type AgentConfig = {
|
|
46
|
-
callModel: CallModel;
|
|
47
|
-
prompt: string;
|
|
48
|
-
tools: Tool[];
|
|
49
|
-
toolUseApprover: ToolUseApprover;
|
|
50
|
-
agentRoles: Map<string, AgentRole>;
|
|
51
|
-
modelCostConfig?: CostConfig;
|
|
52
|
-
};
|
package/src/agent.mjs
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Agent, AgentConfig, AgentEventEmitter, UserEventEmitter } from "./agent"
|
|
3
|
-
* @import { Tool, ToolDefinition } from "./tool"
|
|
4
|
-
* @import { CompactContextInput } from "./tools/compactContext"
|
|
5
|
-
* @import { DelegateToSubagentInput } from "./tools/delegateToSubagent"
|
|
6
|
-
* @import { ReportAsSubagentInput } from "./tools/reportAsSubagent"
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { EventEmitter } from "node:events";
|
|
10
|
-
import fs from "node:fs/promises";
|
|
11
|
-
import { createAgentLoop } from "./agentLoop.mjs";
|
|
12
|
-
import { createStateManager } from "./agentState.mjs";
|
|
13
|
-
import { createCostTracker } from "./costTracker.mjs";
|
|
14
|
-
import { MESSAGES_DUMP_FILE_PATH } from "./env.mjs";
|
|
15
|
-
import { createSubagentManager } from "./subagent.mjs";
|
|
16
|
-
import { createToolExecutor } from "./toolExecutor.mjs";
|
|
17
|
-
import {
|
|
18
|
-
compactContextToolName,
|
|
19
|
-
readMemoryForCompaction,
|
|
20
|
-
} from "./tools/compactContext.mjs";
|
|
21
|
-
import { delegateToSubagentToolName } from "./tools/delegateToSubagent.mjs";
|
|
22
|
-
import { reportAsSubagentToolName } from "./tools/reportAsSubagent.mjs";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {AgentConfig} config
|
|
26
|
-
* @returns {Agent}
|
|
27
|
-
*/
|
|
28
|
-
export function createAgent({
|
|
29
|
-
callModel,
|
|
30
|
-
prompt,
|
|
31
|
-
tools,
|
|
32
|
-
toolUseApprover,
|
|
33
|
-
agentRoles,
|
|
34
|
-
modelCostConfig,
|
|
35
|
-
}) {
|
|
36
|
-
/** @type {UserEventEmitter} */
|
|
37
|
-
const userEventEmitter = new EventEmitter();
|
|
38
|
-
/** @type {AgentEventEmitter} */
|
|
39
|
-
const agentEventEmitter = new EventEmitter();
|
|
40
|
-
|
|
41
|
-
const costTracker = createCostTracker(modelCostConfig);
|
|
42
|
-
|
|
43
|
-
agentEventEmitter.on("providerTokenUsage", (usage) => {
|
|
44
|
-
costTracker.recordUsage(usage);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const stateManager = createStateManager(
|
|
48
|
-
[
|
|
49
|
-
{
|
|
50
|
-
role: "system",
|
|
51
|
-
content: [{ type: "text", text: prompt }],
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
{
|
|
55
|
-
onMessagesAppended: (newMessages) => {
|
|
56
|
-
const lastMessage = newMessages.at(-1);
|
|
57
|
-
if (!lastMessage) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
agentEventEmitter.emit("message", lastMessage);
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const subagentManager = createSubagentManager(agentRoles, {
|
|
66
|
-
onSubagentSwitched: (subagent) => {
|
|
67
|
-
agentEventEmitter.emit("subagentSwitched", subagent);
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* @param {DelegateToSubagentInput} input
|
|
73
|
-
*/
|
|
74
|
-
const delegateToSubagentImpl = async (input) => {
|
|
75
|
-
const result = subagentManager.delegateToSubagent(
|
|
76
|
-
input.name,
|
|
77
|
-
input.goal,
|
|
78
|
-
stateManager.getMessages().length - 1,
|
|
79
|
-
);
|
|
80
|
-
if (!result.success) {
|
|
81
|
-
return new Error(result.error);
|
|
82
|
-
}
|
|
83
|
-
return result.value;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {ReportAsSubagentInput} input
|
|
88
|
-
*/
|
|
89
|
-
const reportAsSubagentImpl = async (input) => {
|
|
90
|
-
const result = await subagentManager.reportAsSubagent(input.memoryPath);
|
|
91
|
-
if (!result.success) {
|
|
92
|
-
return new Error(result.error);
|
|
93
|
-
}
|
|
94
|
-
return result.memoryContent;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @param {Record<string, unknown>} rawInput
|
|
99
|
-
*/
|
|
100
|
-
const compactContextImpl = async (rawInput) => {
|
|
101
|
-
if (subagentManager.isSubagentActive()) {
|
|
102
|
-
return new Error(
|
|
103
|
-
"compact_context cannot be used while running as a subagent. " +
|
|
104
|
-
"Call report_as_subagent to return to the main agent first.",
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
const input = /** @type {CompactContextInput} */ (rawInput);
|
|
108
|
-
return await readMemoryForCompaction(input);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/** @type {Map<string, Tool>} */
|
|
112
|
-
const toolByName = new Map();
|
|
113
|
-
for (const tool of tools) {
|
|
114
|
-
if (tool.def.name === delegateToSubagentToolName && tool.injectImpl) {
|
|
115
|
-
tool.injectImpl(delegateToSubagentImpl);
|
|
116
|
-
}
|
|
117
|
-
if (tool.def.name === reportAsSubagentToolName && tool.injectImpl) {
|
|
118
|
-
tool.injectImpl(reportAsSubagentImpl);
|
|
119
|
-
}
|
|
120
|
-
if (tool.def.name === compactContextToolName && tool.injectImpl) {
|
|
121
|
-
tool.injectImpl(compactContextImpl);
|
|
122
|
-
}
|
|
123
|
-
toolByName.set(tool.def.name, tool);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** @type {ToolDefinition[]} */
|
|
127
|
-
const toolDefs = tools.map(({ def }) => def);
|
|
128
|
-
|
|
129
|
-
const toolExecutor = createToolExecutor(toolByName, {
|
|
130
|
-
exclusiveToolNames: [delegateToSubagentToolName, reportAsSubagentToolName],
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
async function dumpMessages() {
|
|
134
|
-
const filePath = MESSAGES_DUMP_FILE_PATH;
|
|
135
|
-
try {
|
|
136
|
-
await fs.writeFile(
|
|
137
|
-
filePath,
|
|
138
|
-
JSON.stringify(stateManager.getMessages(), null, 2),
|
|
139
|
-
);
|
|
140
|
-
console.log(`Messages dumped to ${filePath}`);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
143
|
-
console.error(`Error dumping messages: ${message}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function loadMessages() {
|
|
148
|
-
const filePath = MESSAGES_DUMP_FILE_PATH;
|
|
149
|
-
try {
|
|
150
|
-
const data = await fs.readFile(filePath, "utf-8");
|
|
151
|
-
const loadedMessages = JSON.parse(data);
|
|
152
|
-
if (Array.isArray(loadedMessages)) {
|
|
153
|
-
// Keep the system message (index 0) and replace the rest
|
|
154
|
-
stateManager.setMessages([
|
|
155
|
-
stateManager.getMessageAt(0),
|
|
156
|
-
...loadedMessages.slice(1),
|
|
157
|
-
]);
|
|
158
|
-
console.log(`Messages loaded from ${filePath}`);
|
|
159
|
-
} else {
|
|
160
|
-
console.error("Error loading messages: Invalid format in file.");
|
|
161
|
-
}
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (error instanceof Error) {
|
|
164
|
-
console.error(`Error loading messages: ${error.message}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Pause signal: set by Ctrl-C during agent execution, checked after each tool batch completes
|
|
170
|
-
let paused = false;
|
|
171
|
-
/** @type {import("./agentLoop.mjs").PauseSignal} */
|
|
172
|
-
const pauseSignal = {
|
|
173
|
-
isPaused: () => paused,
|
|
174
|
-
reset: () => {
|
|
175
|
-
paused = false;
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const agentLoop = createAgentLoop({
|
|
180
|
-
callModel,
|
|
181
|
-
stateManager,
|
|
182
|
-
toolDefs,
|
|
183
|
-
toolExecutor,
|
|
184
|
-
agentEventEmitter,
|
|
185
|
-
toolUseApprover,
|
|
186
|
-
subagentManager,
|
|
187
|
-
pauseSignal,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
userEventEmitter.on("userInput", agentLoop.handleUserInput);
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
userEventEmitter,
|
|
194
|
-
agentEventEmitter,
|
|
195
|
-
agentCommands: {
|
|
196
|
-
dumpMessages,
|
|
197
|
-
loadMessages,
|
|
198
|
-
getCostSummary: () => costTracker.calculateCost(),
|
|
199
|
-
pauseAutoApprove: () => {
|
|
200
|
-
paused = true;
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
}
|
package/src/agentLoop.mjs
DELETED
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { AgentEventEmitter } from "./agent"
|
|
3
|
-
* @import { CallModel, MessageContentText, MessageContentImage, MessageContentToolResult, PartialMessageContent, UserMessage, MessageContentToolUse } from "./model"
|
|
4
|
-
* @import { ToolDefinition, ToolUseApprover } from "./tool"
|
|
5
|
-
* @import { SubagentManager } from "./subagent.mjs"
|
|
6
|
-
* @import { StateManager } from "./agentState.mjs"
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { styleText } from "node:util";
|
|
10
|
-
import { compactContextToolName } from "./tools/compactContext.mjs";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* If compact_context was called successfully, discard the prior conversation
|
|
14
|
-
* (keeping only the system prompt) and append the tool result as a standard
|
|
15
|
-
* user message so the model can resume from the reloaded memory file.
|
|
16
|
-
* @param {StateManager} stateManager
|
|
17
|
-
* @param {MessageContentToolUse[]} toolUseParts
|
|
18
|
-
* @param {MessageContentToolResult[]} toolResults
|
|
19
|
-
* @returns {boolean} true if compact was applied
|
|
20
|
-
*/
|
|
21
|
-
function applyCompactContextIfCalled(stateManager, toolUseParts, toolResults) {
|
|
22
|
-
const compactToolUse = toolUseParts.find(
|
|
23
|
-
(t) => t.toolName === compactContextToolName,
|
|
24
|
-
);
|
|
25
|
-
if (!compactToolUse) return false;
|
|
26
|
-
|
|
27
|
-
const compactResult = toolResults.find(
|
|
28
|
-
(r) => r.toolUseId === compactToolUse.toolUseId,
|
|
29
|
-
);
|
|
30
|
-
if (!compactResult || compactResult.isError) return false;
|
|
31
|
-
|
|
32
|
-
const systemMessage = stateManager.getMessageAt(0);
|
|
33
|
-
if (!systemMessage) return false;
|
|
34
|
-
|
|
35
|
-
stateManager.setMessages([systemMessage]);
|
|
36
|
-
stateManager.appendMessages([
|
|
37
|
-
{ role: "user", content: compactResult.content },
|
|
38
|
-
]);
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @typedef {Object} PauseSignal
|
|
44
|
-
* @property {() => boolean} isPaused - Returns true if auto-approve should be paused
|
|
45
|
-
* @property {() => void} reset - Resets the paused state
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @typedef {Object} AgentLoopConfig
|
|
50
|
-
* @property {CallModel} callModel - Function to call the language model
|
|
51
|
-
* @property {StateManager} stateManager - State manager for message handling
|
|
52
|
-
* @property {ToolDefinition[]} toolDefs - Tool definitions for the model
|
|
53
|
-
* @property {import("./toolExecutor.mjs").ToolExecutor} toolExecutor - Tool executor instance
|
|
54
|
-
* @property {AgentEventEmitter} agentEventEmitter - Event emitter for agent events
|
|
55
|
-
* @property {ToolUseApprover} toolUseApprover - Tool use approval checker
|
|
56
|
-
* @property {SubagentManager} subagentManager - Subagent manager instance
|
|
57
|
-
* @property {PauseSignal} pauseSignal - Signal to pause auto-approve after current tool completes
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {ReturnType<typeof createAgentLoop>} AgentLoop
|
|
62
|
-
*/
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Create an agent loop handler
|
|
66
|
-
* @param {AgentLoopConfig} config
|
|
67
|
-
*/
|
|
68
|
-
export function createAgentLoop({
|
|
69
|
-
callModel,
|
|
70
|
-
stateManager,
|
|
71
|
-
toolDefs,
|
|
72
|
-
toolExecutor,
|
|
73
|
-
agentEventEmitter,
|
|
74
|
-
toolUseApprover,
|
|
75
|
-
subagentManager,
|
|
76
|
-
pauseSignal,
|
|
77
|
-
}) {
|
|
78
|
-
const inputHandler = createInputHandler({
|
|
79
|
-
stateManager,
|
|
80
|
-
toolExecutor,
|
|
81
|
-
subagentManager,
|
|
82
|
-
toolUseApprover,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Handle user input and run the agent turn loop
|
|
87
|
-
* @param {(MessageContentText | MessageContentImage)[]} input - User input content
|
|
88
|
-
* @returns {Promise<void>}
|
|
89
|
-
*/
|
|
90
|
-
async function handleUserInput(input) {
|
|
91
|
-
pauseSignal.reset();
|
|
92
|
-
toolUseApprover.resetApprovalCount();
|
|
93
|
-
await inputHandler.handle(input);
|
|
94
|
-
await runTurnLoop();
|
|
95
|
-
agentEventEmitter.emit("turnEnd");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Run the main agent turn loop
|
|
100
|
-
* @returns {Promise<void>}
|
|
101
|
-
*/
|
|
102
|
-
async function runTurnLoop() {
|
|
103
|
-
let thinkingLoops = 0;
|
|
104
|
-
const maxThinkingLoops = 5;
|
|
105
|
-
|
|
106
|
-
while (true) {
|
|
107
|
-
// Check if auto-approve was paused by Ctrl-C during tool execution
|
|
108
|
-
if (pauseSignal.isPaused()) {
|
|
109
|
-
pauseSignal.reset();
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const modelOutput = await callModel({
|
|
114
|
-
messages: stateManager.getMessages(),
|
|
115
|
-
tools: toolDefs,
|
|
116
|
-
/**
|
|
117
|
-
* @param {PartialMessageContent} partialContent
|
|
118
|
-
*/
|
|
119
|
-
onPartialMessageContent: (partialContent) => {
|
|
120
|
-
agentEventEmitter.emit("partialMessageContent", partialContent);
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (modelOutput instanceof Error) {
|
|
125
|
-
agentEventEmitter.emit("error", modelOutput);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const { message: assistantMessage, providerTokenUsage } = modelOutput;
|
|
130
|
-
stateManager.appendMessages([assistantMessage]);
|
|
131
|
-
if (providerTokenUsage) {
|
|
132
|
-
agentEventEmitter.emit("providerTokenUsage", providerTokenUsage);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Gemini may stop with "thinking" -> continue
|
|
136
|
-
const lastContent = assistantMessage.content.at(-1);
|
|
137
|
-
if (lastContent?.type === "thinking") {
|
|
138
|
-
thinkingLoops += 1;
|
|
139
|
-
if (thinkingLoops > maxThinkingLoops) {
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
stateManager.appendMessages([
|
|
144
|
-
{
|
|
145
|
-
role: "user",
|
|
146
|
-
content: [{ type: "text", text: "System: Continue" }],
|
|
147
|
-
},
|
|
148
|
-
]);
|
|
149
|
-
console.error(
|
|
150
|
-
styleText(
|
|
151
|
-
"yellow",
|
|
152
|
-
`\nModel is thinking. Sending "System: Continue" (Loop: ${thinkingLoops}/${maxThinkingLoops})`,
|
|
153
|
-
),
|
|
154
|
-
);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const toolUseParts = assistantMessage.content.filter(
|
|
159
|
-
(part) => part.type === "tool_use",
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// No tool use -> turn end
|
|
163
|
-
if (toolUseParts.length === 0) {
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const validation = toolExecutor.validateBatch(toolUseParts);
|
|
168
|
-
if (!validation.isValid) {
|
|
169
|
-
stateManager.appendMessages([
|
|
170
|
-
{
|
|
171
|
-
role: "user",
|
|
172
|
-
content: validation.toolResults,
|
|
173
|
-
},
|
|
174
|
-
]);
|
|
175
|
-
if (validation.errorMessage) {
|
|
176
|
-
console.error(styleText("yellow", validation.errorMessage));
|
|
177
|
-
}
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Approve tool use
|
|
182
|
-
const decisions = toolUseParts.map(toolUseApprover.isAllowedToolUse);
|
|
183
|
-
|
|
184
|
-
const hasDeniedToolUse = decisions.some((d) => d.action === "deny");
|
|
185
|
-
if (hasDeniedToolUse) {
|
|
186
|
-
/** @type {MessageContentToolResult[]} */
|
|
187
|
-
const toolResults = toolUseParts.map((toolUse, index) => {
|
|
188
|
-
const decision = decisions[index];
|
|
189
|
-
const rejectionMessage =
|
|
190
|
-
decision.action === "deny"
|
|
191
|
-
? `Tool call rejected. ${decision.reason || ""}`.trim()
|
|
192
|
-
: "Tool call rejected due to other denied tool calls";
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
type: "tool_result",
|
|
196
|
-
toolUseId: toolUse.toolUseId,
|
|
197
|
-
toolName: toolUse.toolName,
|
|
198
|
-
content: [{ type: "text", text: rejectionMessage }],
|
|
199
|
-
isError: true,
|
|
200
|
-
};
|
|
201
|
-
});
|
|
202
|
-
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const isAllToolUseApproved = decisions.every((d) => d.action === "allow");
|
|
207
|
-
if (!isAllToolUseApproved) {
|
|
208
|
-
agentEventEmitter.emit("toolUseRequest");
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const executionResult = await toolExecutor.executeBatch(toolUseParts);
|
|
213
|
-
|
|
214
|
-
if (!executionResult.success) {
|
|
215
|
-
stateManager.appendMessages([
|
|
216
|
-
{
|
|
217
|
-
role: "user",
|
|
218
|
-
content: executionResult.errors,
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
role: "user",
|
|
222
|
-
content: [
|
|
223
|
-
{
|
|
224
|
-
type: "text",
|
|
225
|
-
text: executionResult.errorMessage,
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
},
|
|
229
|
-
]);
|
|
230
|
-
console.error(styleText("yellow", executionResult.errorMessage));
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const toolResults = executionResult.results;
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
applyCompactContextIfCalled(stateManager, toolUseParts, toolResults)
|
|
238
|
-
) {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const result = subagentManager.processToolResults(
|
|
243
|
-
toolUseParts,
|
|
244
|
-
toolResults,
|
|
245
|
-
stateManager.getMessages(),
|
|
246
|
-
);
|
|
247
|
-
stateManager.setMessages(result.messages);
|
|
248
|
-
if (result.newMessage) {
|
|
249
|
-
stateManager.appendMessages([result.newMessage]);
|
|
250
|
-
} else {
|
|
251
|
-
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
handleUserInput,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* @typedef {Object} InputHandlerContext
|
|
263
|
-
* @property {StateManager} stateManager
|
|
264
|
-
* @property {import("./toolExecutor.mjs").ToolExecutor} toolExecutor
|
|
265
|
-
* @property {import("./subagent.mjs").SubagentManager} subagentManager
|
|
266
|
-
* @property {import("./tool.d.ts").ToolUseApprover} toolUseApprover
|
|
267
|
-
*/
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* @typedef {ReturnType<typeof createInputHandler>} InputHandler
|
|
271
|
-
*/
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Create an input handler.
|
|
275
|
-
*
|
|
276
|
-
* @param {InputHandlerContext} context
|
|
277
|
-
*/
|
|
278
|
-
export function createInputHandler(context) {
|
|
279
|
-
const { stateManager, toolExecutor, subagentManager, toolUseApprover } =
|
|
280
|
-
context;
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Determine input type based on current state and input.
|
|
284
|
-
* @param {UserMessage["content"]} input
|
|
285
|
-
* @returns {'toolApproval' | 'resume' | 'text'}
|
|
286
|
-
*/
|
|
287
|
-
function determineInputType(input) {
|
|
288
|
-
const lastMessage = stateManager.getMessageAt(-1);
|
|
289
|
-
|
|
290
|
-
// Check if there's a pending tool call
|
|
291
|
-
if (lastMessage?.content.some((part) => part.type === "tool_use")) {
|
|
292
|
-
return "toolApproval";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
input.length === 1 &&
|
|
297
|
-
input[0].type === "text" &&
|
|
298
|
-
input[0].text.toLowerCase() === "/resume"
|
|
299
|
-
) {
|
|
300
|
-
return "resume";
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return "text";
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Handle tool approval/rejection input.
|
|
308
|
-
* @param {UserMessage["content"]} input
|
|
309
|
-
*/
|
|
310
|
-
async function handleToolApproval(input) {
|
|
311
|
-
const lastMessage = stateManager.getMessageAt(-1);
|
|
312
|
-
if (!lastMessage) return;
|
|
313
|
-
|
|
314
|
-
/** @type {MessageContentToolUse[]} */
|
|
315
|
-
const toolUseParts = lastMessage.content.filter(
|
|
316
|
-
(part) => part.type === "tool_use",
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
const isApproval =
|
|
320
|
-
input.length === 1 &&
|
|
321
|
-
input[0].type === "text" &&
|
|
322
|
-
input[0].text.toLocaleLowerCase().match(/^(yes|y|y)$/i);
|
|
323
|
-
|
|
324
|
-
if (isApproval) {
|
|
325
|
-
if (
|
|
326
|
-
/** @type {MessageContentText} */ (input[0]).text.match(/^(YES|Y)$/)
|
|
327
|
-
) {
|
|
328
|
-
for (const toolUse of toolUseParts) {
|
|
329
|
-
toolUseApprover.allowToolUse(toolUse);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const executionResult = await toolExecutor.executeBatch(toolUseParts);
|
|
334
|
-
if (!executionResult.success) {
|
|
335
|
-
stateManager.appendMessages([
|
|
336
|
-
{ role: "user", content: executionResult.errors },
|
|
337
|
-
]);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const toolResults = executionResult.results;
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
applyCompactContextIfCalled(stateManager, toolUseParts, toolResults)
|
|
345
|
-
) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const result = subagentManager.processToolResults(
|
|
350
|
-
toolUseParts,
|
|
351
|
-
toolResults,
|
|
352
|
-
stateManager.getMessages(),
|
|
353
|
-
);
|
|
354
|
-
stateManager.setMessages(result.messages);
|
|
355
|
-
|
|
356
|
-
if (result.newMessage) {
|
|
357
|
-
stateManager.appendMessages([result.newMessage]);
|
|
358
|
-
} else {
|
|
359
|
-
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
360
|
-
}
|
|
361
|
-
} else {
|
|
362
|
-
// Rejected
|
|
363
|
-
/** @type {MessageContentToolResult[]} */
|
|
364
|
-
const toolResults = toolUseParts.map((toolUse) => ({
|
|
365
|
-
type: "tool_result",
|
|
366
|
-
toolUseId: toolUse.toolUseId,
|
|
367
|
-
toolName: toolUse.toolName,
|
|
368
|
-
content: [{ type: "text", text: "Tool call rejected" }],
|
|
369
|
-
isError: true,
|
|
370
|
-
}));
|
|
371
|
-
|
|
372
|
-
stateManager.appendMessages([
|
|
373
|
-
{ role: "user", content: toolResults },
|
|
374
|
-
{
|
|
375
|
-
role: "user",
|
|
376
|
-
content: input,
|
|
377
|
-
},
|
|
378
|
-
]);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function handleResume() {
|
|
383
|
-
// Resume the conversation stopped by unexpected error, etc.
|
|
384
|
-
// No state changes needed
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* @param {UserMessage["content"]} input
|
|
389
|
-
*/
|
|
390
|
-
async function handleText(input) {
|
|
391
|
-
stateManager.appendMessages([
|
|
392
|
-
{
|
|
393
|
-
role: "user",
|
|
394
|
-
content: input,
|
|
395
|
-
},
|
|
396
|
-
]);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
/**
|
|
401
|
-
* @param {UserMessage["content"]} input
|
|
402
|
-
*/
|
|
403
|
-
async handle(input) {
|
|
404
|
-
const inputType = determineInputType(input);
|
|
405
|
-
|
|
406
|
-
switch (inputType) {
|
|
407
|
-
case "toolApproval":
|
|
408
|
-
await handleToolApproval(input);
|
|
409
|
-
break;
|
|
410
|
-
case "resume":
|
|
411
|
-
await handleResume();
|
|
412
|
-
break;
|
|
413
|
-
case "text":
|
|
414
|
-
await handleText(input);
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
};
|
|
419
|
-
}
|