@iinm/plain-agent 1.0.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/.config/agents.library/code-simplifier.md +5 -0
- package/.config/agents.library/qa-engineer.md +74 -0
- package/.config/agents.library/software-architect.md +278 -0
- package/.config/agents.predefined/worker.md +3 -0
- package/.config/config.predefined.json +825 -0
- package/.config/prompts.library/code-review.md +8 -0
- package/.config/prompts.library/feature-dev.md +6 -0
- package/.config/prompts.predefined/shortcuts/commit-by-user.md +9 -0
- package/.config/prompts.predefined/shortcuts/commit.md +10 -0
- package/.config/prompts.predefined/shortcuts/general-question.md +6 -0
- package/LICENSE +21 -0
- package/README.md +624 -0
- package/bin/plain +3 -0
- package/bin/plain-interrupt +6 -0
- package/bin/plain-notify-desktop +19 -0
- package/bin/plain-notify-terminal-bell +3 -0
- package/package.json +57 -0
- package/sandbox/bin/plain-sandbox +972 -0
- package/src/agent.d.ts +48 -0
- package/src/agent.mjs +159 -0
- package/src/agentLoop.mjs +369 -0
- package/src/agentState.mjs +41 -0
- package/src/cliArgs.mjs +45 -0
- package/src/cliFormatter.mjs +217 -0
- package/src/cliInteractive.mjs +739 -0
- package/src/config.d.ts +48 -0
- package/src/config.mjs +168 -0
- package/src/context/consumeInterruptMessage.mjs +30 -0
- package/src/context/loadAgentRoles.mjs +272 -0
- package/src/context/loadPrompts.mjs +312 -0
- package/src/context/loadUserMessageContext.mjs +147 -0
- package/src/env.mjs +46 -0
- package/src/main.mjs +202 -0
- package/src/mcp.mjs +202 -0
- package/src/model.d.ts +109 -0
- package/src/modelCaller.mjs +29 -0
- package/src/modelDefinition.d.ts +73 -0
- package/src/prompt.mjs +128 -0
- package/src/providers/anthropic.d.ts +248 -0
- package/src/providers/anthropic.mjs +596 -0
- package/src/providers/gemini.d.ts +208 -0
- package/src/providers/gemini.mjs +752 -0
- package/src/providers/openai.d.ts +281 -0
- package/src/providers/openai.mjs +551 -0
- package/src/providers/openaiCompatible.d.ts +147 -0
- package/src/providers/openaiCompatible.mjs +658 -0
- package/src/providers/platform/azure.mjs +42 -0
- package/src/providers/platform/bedrock.mjs +74 -0
- package/src/providers/platform/googleCloud.mjs +34 -0
- package/src/subagent.mjs +247 -0
- package/src/tmpfile.mjs +27 -0
- package/src/tool.d.ts +74 -0
- package/src/toolExecutor.mjs +236 -0
- package/src/toolInputValidator.mjs +183 -0
- package/src/toolUseApprover.mjs +98 -0
- package/src/tools/askGoogle.mjs +135 -0
- package/src/tools/delegateToSubagent.d.ts +4 -0
- package/src/tools/delegateToSubagent.mjs +48 -0
- package/src/tools/execCommand.d.ts +22 -0
- package/src/tools/execCommand.mjs +200 -0
- package/src/tools/fetchWebPage.mjs +96 -0
- package/src/tools/patchFile.d.ts +4 -0
- package/src/tools/patchFile.mjs +96 -0
- package/src/tools/reportAsSubagent.d.ts +3 -0
- package/src/tools/reportAsSubagent.mjs +44 -0
- package/src/tools/tavilySearch.d.ts +6 -0
- package/src/tools/tavilySearch.mjs +57 -0
- package/src/tools/tmuxCommand.d.ts +14 -0
- package/src/tools/tmuxCommand.mjs +194 -0
- package/src/tools/writeFile.d.ts +4 -0
- package/src/tools/writeFile.mjs +56 -0
- package/src/utils/evalJSONConfig.mjs +48 -0
- package/src/utils/matchValue.d.ts +6 -0
- package/src/utils/matchValue.mjs +40 -0
- package/src/utils/noThrow.mjs +31 -0
- package/src/utils/notify.mjs +28 -0
- package/src/utils/parseFileRange.mjs +18 -0
- package/src/utils/readFileRange.mjs +33 -0
- package/src/utils/retryOnError.mjs +41 -0
package/src/agent.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EventEmitter } from "node:events";
|
|
2
|
+
import type {
|
|
3
|
+
CallModel,
|
|
4
|
+
Message,
|
|
5
|
+
MessageContentImage,
|
|
6
|
+
MessageContentText,
|
|
7
|
+
PartialMessageContent,
|
|
8
|
+
ProviderTokenUsage,
|
|
9
|
+
} from "./model";
|
|
10
|
+
import type { Tool, ToolUseApprover } from "./tool";
|
|
11
|
+
import type { AgentRole } from "./context/loadAgentRoles.mjs";
|
|
12
|
+
|
|
13
|
+
export type Agent = {
|
|
14
|
+
userEventEmitter: UserEventEmitter;
|
|
15
|
+
agentEventEmitter: AgentEventEmitter;
|
|
16
|
+
agentCommands: AgentCommands;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type AgentCommands = {
|
|
20
|
+
dumpMessages: () => Promise<void>;
|
|
21
|
+
loadMessages: () => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type UserEventMap = {
|
|
25
|
+
userInput: [(MessageContentText | MessageContentImage)[]];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type UserEventEmitter = EventEmitter<UserEventMap>;
|
|
29
|
+
|
|
30
|
+
type AgentEventMap = {
|
|
31
|
+
message: [Message];
|
|
32
|
+
partialMessageContent: [PartialMessageContent];
|
|
33
|
+
error: [Error];
|
|
34
|
+
toolUseRequest: [];
|
|
35
|
+
turnEnd: [];
|
|
36
|
+
providerTokenUsage: [ProviderTokenUsage];
|
|
37
|
+
subagentSwitched: [{ name: string } | null];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type AgentEventEmitter = EventEmitter<AgentEventMap>;
|
|
41
|
+
|
|
42
|
+
export type AgentConfig = {
|
|
43
|
+
callModel: CallModel;
|
|
44
|
+
prompt: string;
|
|
45
|
+
tools: Tool[];
|
|
46
|
+
toolUseApprover: ToolUseApprover;
|
|
47
|
+
agentRoles: Map<string, AgentRole>;
|
|
48
|
+
};
|
package/src/agent.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Agent, AgentConfig, AgentEventEmitter, UserEventEmitter } from "./agent"
|
|
3
|
+
* @import { Tool, ToolDefinition } from "./tool"
|
|
4
|
+
* @import { DelegateToSubagentInput } from "./tools/delegateToSubagent"
|
|
5
|
+
* @import { ReportAsSubagentInput } from "./tools/reportAsSubagent"
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import { createAgentLoop } from "./agentLoop.mjs";
|
|
11
|
+
import { createStateManager } from "./agentState.mjs";
|
|
12
|
+
import { MESSAGES_DUMP_FILE_PATH } from "./env.mjs";
|
|
13
|
+
import { createSubagentManager } from "./subagent.mjs";
|
|
14
|
+
import { createToolExecutor } from "./toolExecutor.mjs";
|
|
15
|
+
import { delegateToSubagentToolName } from "./tools/delegateToSubagent.mjs";
|
|
16
|
+
import { reportAsSubagentToolName } from "./tools/reportAsSubagent.mjs";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {AgentConfig} config
|
|
20
|
+
* @returns {Agent}
|
|
21
|
+
*/
|
|
22
|
+
export function createAgent({
|
|
23
|
+
callModel,
|
|
24
|
+
prompt,
|
|
25
|
+
tools,
|
|
26
|
+
toolUseApprover,
|
|
27
|
+
agentRoles,
|
|
28
|
+
}) {
|
|
29
|
+
/** @type {UserEventEmitter} */
|
|
30
|
+
const userEventEmitter = new EventEmitter();
|
|
31
|
+
/** @type {AgentEventEmitter} */
|
|
32
|
+
const agentEventEmitter = new EventEmitter();
|
|
33
|
+
|
|
34
|
+
const stateManager = createStateManager(
|
|
35
|
+
[
|
|
36
|
+
{
|
|
37
|
+
role: "system",
|
|
38
|
+
content: [{ type: "text", text: prompt }],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
{
|
|
42
|
+
onMessagesAppended: (newMessages) => {
|
|
43
|
+
const lastMessage = newMessages.at(-1);
|
|
44
|
+
if (!lastMessage) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
agentEventEmitter.emit("message", lastMessage);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const subagentManager = createSubagentManager(agentRoles, {
|
|
53
|
+
onSubagentSwitched: (subagent) => {
|
|
54
|
+
agentEventEmitter.emit("subagentSwitched", subagent);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {DelegateToSubagentInput} input
|
|
60
|
+
*/
|
|
61
|
+
const delegateToSubagentImpl = async (input) => {
|
|
62
|
+
const result = subagentManager.delegateToSubagent(
|
|
63
|
+
input.name,
|
|
64
|
+
input.goal,
|
|
65
|
+
stateManager.getMessages().length - 1,
|
|
66
|
+
);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
return new Error(result.error);
|
|
69
|
+
}
|
|
70
|
+
return result.value;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {ReportAsSubagentInput} input
|
|
75
|
+
*/
|
|
76
|
+
const reportAsSubagentImpl = async (input) => {
|
|
77
|
+
const result = await subagentManager.reportAsSubagent(input.memoryPath);
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
return new Error(result.error);
|
|
80
|
+
}
|
|
81
|
+
return result.memoryContent;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** @type {Map<string, Tool>} */
|
|
85
|
+
const toolByName = new Map();
|
|
86
|
+
for (const tool of tools) {
|
|
87
|
+
if (tool.def.name === delegateToSubagentToolName && tool.injectImpl) {
|
|
88
|
+
tool.injectImpl(delegateToSubagentImpl);
|
|
89
|
+
}
|
|
90
|
+
if (tool.def.name === reportAsSubagentToolName && tool.injectImpl) {
|
|
91
|
+
tool.injectImpl(reportAsSubagentImpl);
|
|
92
|
+
}
|
|
93
|
+
toolByName.set(tool.def.name, tool);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @type {ToolDefinition[]} */
|
|
97
|
+
const toolDefs = tools.map(({ def }) => def);
|
|
98
|
+
|
|
99
|
+
const toolExecutor = createToolExecutor(toolByName, {
|
|
100
|
+
exclusiveToolNames: [delegateToSubagentToolName, reportAsSubagentToolName],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
async function dumpMessages() {
|
|
104
|
+
const filePath = MESSAGES_DUMP_FILE_PATH;
|
|
105
|
+
try {
|
|
106
|
+
await fs.writeFile(
|
|
107
|
+
filePath,
|
|
108
|
+
JSON.stringify(stateManager.getMessages(), null, 2),
|
|
109
|
+
);
|
|
110
|
+
console.log(`Messages dumped to ${filePath}`);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
console.error(`Error dumping messages: ${message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function loadMessages() {
|
|
118
|
+
const filePath = MESSAGES_DUMP_FILE_PATH;
|
|
119
|
+
try {
|
|
120
|
+
const data = await fs.readFile(filePath, "utf-8");
|
|
121
|
+
const loadedMessages = JSON.parse(data);
|
|
122
|
+
if (Array.isArray(loadedMessages)) {
|
|
123
|
+
// Keep the system message (index 0) and replace the rest
|
|
124
|
+
stateManager.setMessages([
|
|
125
|
+
stateManager.getMessageAt(0),
|
|
126
|
+
...loadedMessages.slice(1),
|
|
127
|
+
]);
|
|
128
|
+
console.log(`Messages loaded from ${filePath}`);
|
|
129
|
+
} else {
|
|
130
|
+
console.error("Error loading messages: Invalid format in file.");
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof Error) {
|
|
134
|
+
console.error(`Error loading messages: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const agentLoop = createAgentLoop({
|
|
140
|
+
callModel,
|
|
141
|
+
stateManager,
|
|
142
|
+
toolDefs,
|
|
143
|
+
toolExecutor,
|
|
144
|
+
agentEventEmitter,
|
|
145
|
+
toolUseApprover,
|
|
146
|
+
subagentManager,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
userEventEmitter.on("userInput", agentLoop.handleUserInput);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
userEventEmitter,
|
|
153
|
+
agentEventEmitter,
|
|
154
|
+
agentCommands: {
|
|
155
|
+
dumpMessages,
|
|
156
|
+
loadMessages,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
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 { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} AgentLoopConfig
|
|
14
|
+
* @property {CallModel} callModel - Function to call the language model
|
|
15
|
+
* @property {StateManager} stateManager - State manager for message handling
|
|
16
|
+
* @property {ToolDefinition[]} toolDefs - Tool definitions for the model
|
|
17
|
+
* @property {import("./toolExecutor.mjs").ToolExecutor} toolExecutor - Tool executor instance
|
|
18
|
+
* @property {AgentEventEmitter} agentEventEmitter - Event emitter for agent events
|
|
19
|
+
* @property {ToolUseApprover} toolUseApprover - Tool use approval checker
|
|
20
|
+
* @property {SubagentManager} subagentManager - Subagent manager instance
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {ReturnType<typeof createAgentLoop>} AgentLoop
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create an agent loop handler
|
|
29
|
+
* @param {AgentLoopConfig} config
|
|
30
|
+
*/
|
|
31
|
+
export function createAgentLoop({
|
|
32
|
+
callModel,
|
|
33
|
+
stateManager,
|
|
34
|
+
toolDefs,
|
|
35
|
+
toolExecutor,
|
|
36
|
+
agentEventEmitter,
|
|
37
|
+
toolUseApprover,
|
|
38
|
+
subagentManager,
|
|
39
|
+
}) {
|
|
40
|
+
const inputHandler = createInputHandler({
|
|
41
|
+
stateManager,
|
|
42
|
+
toolExecutor,
|
|
43
|
+
subagentManager,
|
|
44
|
+
toolUseApprover,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle user input and run the agent turn loop
|
|
49
|
+
* @param {(MessageContentText | MessageContentImage)[]} input - User input content
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
async function handleUserInput(input) {
|
|
53
|
+
toolUseApprover.resetApprovalCount();
|
|
54
|
+
await inputHandler.handle(input);
|
|
55
|
+
await runTurnLoop();
|
|
56
|
+
agentEventEmitter.emit("turnEnd");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run the main agent turn loop
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
*/
|
|
63
|
+
async function runTurnLoop() {
|
|
64
|
+
let thinkingLoops = 0;
|
|
65
|
+
const maxThinkingLoops = 5;
|
|
66
|
+
|
|
67
|
+
while (true) {
|
|
68
|
+
const modelOutput = await callModel({
|
|
69
|
+
messages: stateManager.getMessages(),
|
|
70
|
+
tools: toolDefs,
|
|
71
|
+
/**
|
|
72
|
+
* @param {PartialMessageContent} partialContent
|
|
73
|
+
*/
|
|
74
|
+
onPartialMessageContent: (partialContent) => {
|
|
75
|
+
agentEventEmitter.emit("partialMessageContent", partialContent);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (modelOutput instanceof Error) {
|
|
80
|
+
agentEventEmitter.emit("error", modelOutput);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { message: assistantMessage, providerTokenUsage } = modelOutput;
|
|
85
|
+
stateManager.appendMessages([assistantMessage]);
|
|
86
|
+
agentEventEmitter.emit("providerTokenUsage", providerTokenUsage);
|
|
87
|
+
|
|
88
|
+
// Gemini may stop with "thinking" -> continue
|
|
89
|
+
const lastContent = assistantMessage.content.at(-1);
|
|
90
|
+
if (lastContent?.type === "thinking") {
|
|
91
|
+
thinkingLoops += 1;
|
|
92
|
+
if (thinkingLoops > maxThinkingLoops) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
stateManager.appendMessages([
|
|
97
|
+
{
|
|
98
|
+
role: "user",
|
|
99
|
+
content: [{ type: "text", text: "System: Continue" }],
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
console.error(
|
|
103
|
+
styleText(
|
|
104
|
+
"yellow",
|
|
105
|
+
`\nModel is thinking. Sending "System: Continue" (Loop: ${thinkingLoops}/${maxThinkingLoops})`,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const toolUseParts = assistantMessage.content.filter(
|
|
112
|
+
(part) => part.type === "tool_use",
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// No tool use -> turn end
|
|
116
|
+
if (toolUseParts.length === 0) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const validation = toolExecutor.validateBatch(toolUseParts);
|
|
121
|
+
if (!validation.isValid) {
|
|
122
|
+
stateManager.appendMessages([
|
|
123
|
+
{
|
|
124
|
+
role: "user",
|
|
125
|
+
content: validation.toolResults,
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
if (validation.errorMessage) {
|
|
129
|
+
console.error(styleText("yellow", validation.errorMessage));
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Approve tool use
|
|
135
|
+
const decisions = toolUseParts.map(toolUseApprover.isAllowedToolUse);
|
|
136
|
+
|
|
137
|
+
const hasDeniedToolUse = decisions.some((d) => d.action === "deny");
|
|
138
|
+
if (hasDeniedToolUse) {
|
|
139
|
+
/** @type {MessageContentToolResult[]} */
|
|
140
|
+
const toolResults = toolUseParts.map((toolUse, index) => {
|
|
141
|
+
const decision = decisions[index];
|
|
142
|
+
const rejectionMessage =
|
|
143
|
+
decision.action === "deny"
|
|
144
|
+
? `Tool call rejected. ${decision.reason || ""}`.trim()
|
|
145
|
+
: "Tool call rejected due to other denied tool calls";
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
type: "tool_result",
|
|
149
|
+
toolUseId: toolUse.toolUseId,
|
|
150
|
+
toolName: toolUse.toolName,
|
|
151
|
+
content: [{ type: "text", text: rejectionMessage }],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isAllToolUseApproved = decisions.every((d) => d.action === "allow");
|
|
160
|
+
if (!isAllToolUseApproved) {
|
|
161
|
+
agentEventEmitter.emit("toolUseRequest");
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const executionResult = await toolExecutor.executeBatch(toolUseParts);
|
|
166
|
+
|
|
167
|
+
if (!executionResult.success) {
|
|
168
|
+
stateManager.appendMessages([
|
|
169
|
+
{
|
|
170
|
+
role: "user",
|
|
171
|
+
content: executionResult.errors,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
role: "user",
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: executionResult.errorMessage,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
183
|
+
console.error(styleText("yellow", executionResult.errorMessage));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const toolResults = executionResult.results;
|
|
188
|
+
|
|
189
|
+
const result = subagentManager.processToolResults(
|
|
190
|
+
toolUseParts,
|
|
191
|
+
toolResults,
|
|
192
|
+
stateManager.getMessages(),
|
|
193
|
+
);
|
|
194
|
+
stateManager.setMessages(result.messages);
|
|
195
|
+
if (result.newMessage) {
|
|
196
|
+
stateManager.appendMessages([result.newMessage]);
|
|
197
|
+
} else {
|
|
198
|
+
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const interruptMessage = await consumeInterruptMessage();
|
|
202
|
+
if (interruptMessage) {
|
|
203
|
+
stateManager.appendMessages([
|
|
204
|
+
{
|
|
205
|
+
role: "user",
|
|
206
|
+
content: [{ type: "text", text: interruptMessage }],
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
handleUserInput,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @typedef {Object} InputHandlerContext
|
|
220
|
+
* @property {StateManager} stateManager
|
|
221
|
+
* @property {import("./toolExecutor.mjs").ToolExecutor} toolExecutor
|
|
222
|
+
* @property {import("./subagent.mjs").SubagentManager} subagentManager
|
|
223
|
+
* @property {import("./tool.d.ts").ToolUseApprover} toolUseApprover
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @typedef {ReturnType<typeof createInputHandler>} InputHandler
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create an input handler.
|
|
232
|
+
*
|
|
233
|
+
* @param {InputHandlerContext} context
|
|
234
|
+
*/
|
|
235
|
+
export function createInputHandler(context) {
|
|
236
|
+
const { stateManager, toolExecutor, subagentManager, toolUseApprover } =
|
|
237
|
+
context;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Determine input type based on current state and input.
|
|
241
|
+
* @param {UserMessage["content"]} input
|
|
242
|
+
* @returns {'toolApproval' | 'resume' | 'text'}
|
|
243
|
+
*/
|
|
244
|
+
function determineInputType(input) {
|
|
245
|
+
const lastMessage = stateManager.getMessageAt(-1);
|
|
246
|
+
|
|
247
|
+
// Check if there's a pending tool call
|
|
248
|
+
if (lastMessage?.content.some((part) => part.type === "tool_use")) {
|
|
249
|
+
return "toolApproval";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
input.length === 1 &&
|
|
254
|
+
input[0].type === "text" &&
|
|
255
|
+
input[0].text.toLowerCase() === "/resume"
|
|
256
|
+
) {
|
|
257
|
+
return "resume";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return "text";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Handle tool approval/rejection input.
|
|
265
|
+
* @param {UserMessage["content"]} input
|
|
266
|
+
*/
|
|
267
|
+
async function handleToolApproval(input) {
|
|
268
|
+
const lastMessage = stateManager.getMessageAt(-1);
|
|
269
|
+
if (!lastMessage) return;
|
|
270
|
+
|
|
271
|
+
/** @type {MessageContentToolUse[]} */
|
|
272
|
+
const toolUseParts = lastMessage.content.filter(
|
|
273
|
+
(part) => part.type === "tool_use",
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const isApproval =
|
|
277
|
+
input.length === 1 &&
|
|
278
|
+
input[0].type === "text" &&
|
|
279
|
+
input[0].text.toLocaleLowerCase().match(/^(yes|y|y)$/i);
|
|
280
|
+
|
|
281
|
+
if (isApproval) {
|
|
282
|
+
if (
|
|
283
|
+
/** @type {MessageContentText} */ (input[0]).text.match(/^(YES|Y)$/)
|
|
284
|
+
) {
|
|
285
|
+
for (const toolUse of toolUseParts) {
|
|
286
|
+
toolUseApprover.allowToolUse(toolUse);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const executionResult = await toolExecutor.executeBatch(toolUseParts);
|
|
291
|
+
if (!executionResult.success) {
|
|
292
|
+
stateManager.appendMessages([
|
|
293
|
+
{ role: "user", content: executionResult.errors },
|
|
294
|
+
]);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const toolResults = executionResult.results;
|
|
299
|
+
const result = subagentManager.processToolResults(
|
|
300
|
+
toolUseParts,
|
|
301
|
+
toolResults,
|
|
302
|
+
stateManager.getMessages(),
|
|
303
|
+
);
|
|
304
|
+
stateManager.setMessages(result.messages);
|
|
305
|
+
|
|
306
|
+
if (result.newMessage) {
|
|
307
|
+
stateManager.appendMessages([result.newMessage]);
|
|
308
|
+
} else {
|
|
309
|
+
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Rejected
|
|
313
|
+
/** @type {MessageContentToolResult[]} */
|
|
314
|
+
const toolResults = toolUseParts.map((toolUse) => ({
|
|
315
|
+
type: "tool_result",
|
|
316
|
+
toolUseId: toolUse.toolUseId,
|
|
317
|
+
toolName: toolUse.toolName,
|
|
318
|
+
content: [{ type: "text", text: "Tool call rejected" }],
|
|
319
|
+
isError: true,
|
|
320
|
+
}));
|
|
321
|
+
|
|
322
|
+
stateManager.appendMessages([
|
|
323
|
+
{ role: "user", content: toolResults },
|
|
324
|
+
{
|
|
325
|
+
role: "user",
|
|
326
|
+
content: input,
|
|
327
|
+
},
|
|
328
|
+
]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function handleResume() {
|
|
333
|
+
// Resume the conversation stopped by unexpected error, etc.
|
|
334
|
+
// No state changes needed
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @param {UserMessage["content"]} input
|
|
339
|
+
*/
|
|
340
|
+
async function handleText(input) {
|
|
341
|
+
stateManager.appendMessages([
|
|
342
|
+
{
|
|
343
|
+
role: "user",
|
|
344
|
+
content: input,
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
/**
|
|
351
|
+
* @param {UserMessage["content"]} input
|
|
352
|
+
*/
|
|
353
|
+
async handle(input) {
|
|
354
|
+
const inputType = determineInputType(input);
|
|
355
|
+
|
|
356
|
+
switch (inputType) {
|
|
357
|
+
case "toolApproval":
|
|
358
|
+
await handleToolApproval(input);
|
|
359
|
+
break;
|
|
360
|
+
case "resume":
|
|
361
|
+
await handleResume();
|
|
362
|
+
break;
|
|
363
|
+
case "text":
|
|
364
|
+
await handleText(input);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Message } from "./model"
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {ReturnType<typeof createStateManager>} StateManager
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} StateEventHandlers
|
|
11
|
+
* @property {(messages: Message[]) => void} onMessagesAppended
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a state manager for message handling.
|
|
16
|
+
* @param {Message[]} initialMessages
|
|
17
|
+
* @param {StateEventHandlers} handlers
|
|
18
|
+
*/
|
|
19
|
+
export function createStateManager(initialMessages, handlers) {
|
|
20
|
+
/** @type {Message[]} */
|
|
21
|
+
let messages = [...initialMessages];
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
/** Get all messages (immutable copy) */
|
|
25
|
+
getMessages: () => [...messages],
|
|
26
|
+
|
|
27
|
+
/** Get message at specific index (supports -1 for last) */
|
|
28
|
+
getMessageAt: /** @param {number} index */ (index) => messages.at(index),
|
|
29
|
+
|
|
30
|
+
/** Append messages */
|
|
31
|
+
appendMessages: /** @param {Message[]} newMessages */ (newMessages) => {
|
|
32
|
+
messages = [...messages, ...newMessages];
|
|
33
|
+
handlers.onMessagesAppended(newMessages);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/** Replace all messages */
|
|
37
|
+
setMessages: /** @param {Message[]} newMessages */ (newMessages) => {
|
|
38
|
+
messages = [...newMessages];
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/cliArgs.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} CliArgs
|
|
3
|
+
* @property {string|null} model - Model name with variant
|
|
4
|
+
* @property {boolean} showHelp - Whether to show help message
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse command-line arguments.
|
|
9
|
+
* @param {string[]} argv - process.argv or similar
|
|
10
|
+
* @returns {CliArgs}
|
|
11
|
+
*/
|
|
12
|
+
export function parseCliArgs(argv) {
|
|
13
|
+
const args = argv.slice(2);
|
|
14
|
+
/** @type {CliArgs} */
|
|
15
|
+
const result = { model: null, showHelp: false };
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
if (args[i] === "-m" && args[i + 1]) {
|
|
19
|
+
result.model = args[++i];
|
|
20
|
+
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
21
|
+
result.showHelp = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Print help message and exit.
|
|
30
|
+
* @param {number} [exitCode] - Exit code (default: 0)
|
|
31
|
+
*/
|
|
32
|
+
export function printHelp(exitCode = 0) {
|
|
33
|
+
console.log(`
|
|
34
|
+
Usage: agent [options]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
-m <model+variant> Model to use
|
|
38
|
+
-h, --help Show this help message
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
agent -m claude-sonnet-4-6+thinking-16k
|
|
42
|
+
agent -m gpt-5.4+thinking-medium
|
|
43
|
+
`);
|
|
44
|
+
process.exit(exitCode);
|
|
45
|
+
}
|