@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.
Files changed (86) hide show
  1. package/README.md +6 -2
  2. package/bin/plain +1 -1
  3. package/config/config.predefined.json +1 -1
  4. package/config/prompts.predefined/shortcuts/configure.md +1 -1
  5. package/dist/main.mjs +473 -0
  6. package/dist/main.mjs.map +7 -0
  7. package/package.json +5 -7
  8. package/src/agent.d.ts +0 -52
  9. package/src/agent.mjs +0 -204
  10. package/src/agentLoop.mjs +0 -419
  11. package/src/agentState.mjs +0 -41
  12. package/src/claudeCodePlugin.mjs +0 -164
  13. package/src/cliArgs.mjs +0 -175
  14. package/src/cliBatch.mjs +0 -144
  15. package/src/cliCommands.mjs +0 -283
  16. package/src/cliCompleter.mjs +0 -227
  17. package/src/cliCost.mjs +0 -309
  18. package/src/cliFormatter.mjs +0 -413
  19. package/src/cliInteractive.mjs +0 -526
  20. package/src/cliInterruptTransform.mjs +0 -51
  21. package/src/cliMuteTransform.mjs +0 -26
  22. package/src/cliPasteTransform.mjs +0 -183
  23. package/src/config.d.ts +0 -36
  24. package/src/config.mjs +0 -197
  25. package/src/context/loadAgentRoles.mjs +0 -283
  26. package/src/context/loadPrompts.mjs +0 -324
  27. package/src/context/loadUserMessageContext.mjs +0 -147
  28. package/src/costTracker.mjs +0 -210
  29. package/src/env.mjs +0 -44
  30. package/src/main.mjs +0 -278
  31. package/src/mcpClient.mjs +0 -351
  32. package/src/mcpIntegration.mjs +0 -160
  33. package/src/model.d.ts +0 -109
  34. package/src/modelCaller.mjs +0 -32
  35. package/src/modelDefinition.d.ts +0 -92
  36. package/src/prompt.mjs +0 -138
  37. package/src/providers/anthropic.d.ts +0 -248
  38. package/src/providers/anthropic.mjs +0 -587
  39. package/src/providers/bedrock.d.ts +0 -249
  40. package/src/providers/bedrock.mjs +0 -700
  41. package/src/providers/gemini.d.ts +0 -208
  42. package/src/providers/gemini.mjs +0 -754
  43. package/src/providers/openai.d.ts +0 -281
  44. package/src/providers/openai.mjs +0 -544
  45. package/src/providers/openaiCompatible.d.ts +0 -147
  46. package/src/providers/openaiCompatible.mjs +0 -652
  47. package/src/providers/platform/awsSigV4.mjs +0 -184
  48. package/src/providers/platform/azure.mjs +0 -42
  49. package/src/providers/platform/bedrock.mjs +0 -78
  50. package/src/providers/platform/googleCloud.mjs +0 -34
  51. package/src/subagent.mjs +0 -265
  52. package/src/tmpfile.mjs +0 -27
  53. package/src/tool.d.ts +0 -74
  54. package/src/toolExecutor.mjs +0 -236
  55. package/src/toolInputValidator.mjs +0 -183
  56. package/src/toolUseApprover.mjs +0 -99
  57. package/src/tools/askURL.mjs +0 -209
  58. package/src/tools/askWeb.mjs +0 -208
  59. package/src/tools/compactContext.d.ts +0 -4
  60. package/src/tools/compactContext.mjs +0 -87
  61. package/src/tools/delegateToSubagent.d.ts +0 -4
  62. package/src/tools/delegateToSubagent.mjs +0 -48
  63. package/src/tools/execCommand.d.ts +0 -22
  64. package/src/tools/execCommand.mjs +0 -200
  65. package/src/tools/patchFile.d.ts +0 -4
  66. package/src/tools/patchFile.mjs +0 -133
  67. package/src/tools/reportAsSubagent.d.ts +0 -3
  68. package/src/tools/reportAsSubagent.mjs +0 -44
  69. package/src/tools/tmuxCommand.d.ts +0 -14
  70. package/src/tools/tmuxCommand.mjs +0 -194
  71. package/src/tools/writeFile.d.ts +0 -4
  72. package/src/tools/writeFile.mjs +0 -56
  73. package/src/usageStore.mjs +0 -167
  74. package/src/utils/evalJSONConfig.mjs +0 -72
  75. package/src/utils/matchValue.d.ts +0 -6
  76. package/src/utils/matchValue.mjs +0 -40
  77. package/src/utils/noThrow.mjs +0 -31
  78. package/src/utils/notify.mjs +0 -29
  79. package/src/utils/parseFileRange.mjs +0 -18
  80. package/src/utils/readFileRange.mjs +0 -33
  81. package/src/utils/retryOnError.mjs +0 -41
  82. package/src/voiceInput.mjs +0 -61
  83. package/src/voiceInputGemini.mjs +0 -105
  84. package/src/voiceInputOpenAI.mjs +0 -104
  85. package/src/voiceInputSession.mjs +0 -543
  86. 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.2",
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
- "src/**/*.mjs",
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
- }