@iinm/plain-agent 1.11.4 → 1.11.6
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 +1 -1
- package/config/config.predefined.json +2 -25
- package/package.json +1 -1
- package/src/cli/formatter.mjs +52 -16
- package/src/cli/interactive.mjs +4 -1
- package/src/main.mjs +3 -1
- package/src/providers/anthropic.mjs +1 -1
- package/src/providers/bedrock.mjs +1 -1
- package/src/providers/gemini.mjs +17 -9
- package/src/providers/openai.mjs +1 -1
- package/src/providers/openaiCompatible.mjs +20 -13
- package/src/tool.d.ts +14 -0
- package/src/tools/execCommand.mjs +18 -3
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://deepwiki.com/iinm/plain-agent)
|
|
4
4
|
[](https://www.npmjs.com/package/@iinm/plain-agent)
|
|
5
5
|
[](https://packagephobia.com/result?p=@iinm/plain-agent)
|
|
6
|
-
[](https://socket.dev/npm/package/@iinm/plain-agent)
|
|
7
7
|
[](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql)
|
|
8
8
|
|
|
9
9
|
A lightweight terminal-based coding agent focused on safety and low token cost
|
|
@@ -1676,7 +1676,7 @@
|
|
|
1676
1676
|
},
|
|
1677
1677
|
|
|
1678
1678
|
{
|
|
1679
|
-
"name": "minimax-
|
|
1679
|
+
"name": "minimax-m3",
|
|
1680
1680
|
"variant": "fireworks",
|
|
1681
1681
|
"platform": {
|
|
1682
1682
|
"name": "openai-compatible",
|
|
@@ -1685,30 +1685,7 @@
|
|
|
1685
1685
|
"model": {
|
|
1686
1686
|
"format": "openai-messages",
|
|
1687
1687
|
"config": {
|
|
1688
|
-
"model": "accounts/fireworks/models/minimax-
|
|
1689
|
-
}
|
|
1690
|
-
},
|
|
1691
|
-
"cost": {
|
|
1692
|
-
"currency": "USD",
|
|
1693
|
-
"unit": "1M",
|
|
1694
|
-
"prices": {
|
|
1695
|
-
"prompt_tokens": 0.3,
|
|
1696
|
-
"prompt_tokens_details.cached_tokens": -0.24,
|
|
1697
|
-
"completion_tokens": 1.2
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
},
|
|
1701
|
-
{
|
|
1702
|
-
"name": "minimax-m2.7",
|
|
1703
|
-
"variant": "novita",
|
|
1704
|
-
"platform": {
|
|
1705
|
-
"name": "openai-compatible",
|
|
1706
|
-
"variant": "novita"
|
|
1707
|
-
},
|
|
1708
|
-
"model": {
|
|
1709
|
-
"format": "openai-messages",
|
|
1710
|
-
"config": {
|
|
1711
|
-
"model": "minimax/minimax-m2.7"
|
|
1688
|
+
"model": "accounts/fireworks/models/minimax-m3"
|
|
1712
1689
|
}
|
|
1713
1690
|
},
|
|
1714
1691
|
"cost": {
|
package/package.json
CHANGED
package/src/cli/formatter.mjs
CHANGED
|
@@ -18,6 +18,9 @@ import { noThrow } from "../utils/noThrow.mjs";
|
|
|
18
18
|
/** Length above which a single-line arg forces block-form rendering. */
|
|
19
19
|
const ARG_BLOCK_LENGTH_THRESHOLD = 60;
|
|
20
20
|
|
|
21
|
+
/** Total JSON length above which args are rendered in block form even if each individual arg is short. */
|
|
22
|
+
const ARGS_TOTAL_BLOCK_LENGTH_THRESHOLD = 160;
|
|
23
|
+
|
|
21
24
|
/**
|
|
22
25
|
* Format an args array for display.
|
|
23
26
|
* Uses compact JSON for short single-line args; switches to a YAML-style
|
|
@@ -27,18 +30,20 @@ const ARG_BLOCK_LENGTH_THRESHOLD = 60;
|
|
|
27
30
|
* @param {unknown} args
|
|
28
31
|
* @returns {string}
|
|
29
32
|
*/
|
|
30
|
-
export function
|
|
33
|
+
export function formatCommandArgs(args) {
|
|
31
34
|
if (!Array.isArray(args) || args.length === 0) {
|
|
32
35
|
return `args: ${JSON.stringify(args ?? [])}`;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
const needsBlock =
|
|
36
|
-
(
|
|
37
|
-
|
|
38
|
-
(a
|
|
39
|
-
|
|
38
|
+
const needsBlock =
|
|
39
|
+
JSON.stringify(args).length > ARGS_TOTAL_BLOCK_LENGTH_THRESHOLD ||
|
|
40
|
+
args.some(
|
|
41
|
+
(a) =>
|
|
42
|
+
typeof a === "string" &&
|
|
43
|
+
(a.includes("\n") || a.length > ARG_BLOCK_LENGTH_THRESHOLD),
|
|
44
|
+
);
|
|
40
45
|
if (!needsBlock) {
|
|
41
|
-
return `args: ${JSON.stringify(args)}`;
|
|
46
|
+
return `args: ${highlightCommandArgs(JSON.stringify(args))}`;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
const lines = ["args:"];
|
|
@@ -49,30 +54,60 @@ export function formatArgs(args) {
|
|
|
49
54
|
) {
|
|
50
55
|
lines.push(" - |");
|
|
51
56
|
for (const line of arg.split("\n")) {
|
|
52
|
-
lines.push(` ${line}`);
|
|
57
|
+
lines.push(` ${highlightCommandArgs(line)}`);
|
|
53
58
|
}
|
|
54
59
|
} else {
|
|
55
|
-
lines.push(` - ${JSON.stringify(arg)}`);
|
|
60
|
+
lines.push(` - ${highlightCommandArgs(JSON.stringify(arg))}`);
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
return lines.join("\n");
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} args
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function highlightCommandArgs(args) {
|
|
71
|
+
return (
|
|
72
|
+
args
|
|
73
|
+
// --foo
|
|
74
|
+
.replace(
|
|
75
|
+
/(^|\s|")(--[a-zA-Z0-9-]+)(\s|"|$)/gm,
|
|
76
|
+
(_, p1, p2, p3) => p1 + styleText("cyan", p2) + p3,
|
|
77
|
+
)
|
|
78
|
+
// -f
|
|
79
|
+
.replace(
|
|
80
|
+
/(^|\s|")(-[a-zA-Z]+)(\s|"|$)/gm,
|
|
81
|
+
(_, p1, p2, p3) => p1 + styleText("cyan", p2) + p3,
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @import { SandboxModeProvider } from "../tool"
|
|
88
|
+
*/
|
|
89
|
+
|
|
61
90
|
/**
|
|
62
91
|
* Format tool use for display.
|
|
63
92
|
* @param {MessageContentToolUse} toolUse
|
|
93
|
+
* @param {{ execCommandTool?: SandboxModeProvider }} [options]
|
|
64
94
|
* @returns {Promise<string>}
|
|
65
95
|
*/
|
|
66
|
-
export async function formatToolUse(toolUse) {
|
|
96
|
+
export async function formatToolUse(toolUse, options = {}) {
|
|
67
97
|
const { toolName, input } = toolUse;
|
|
68
98
|
|
|
69
99
|
if (toolName === "exec_command") {
|
|
70
100
|
/** @type {Partial<ExecCommandInput>} */
|
|
71
101
|
const execCommandInput = input;
|
|
102
|
+
const mode = options.execCommandTool?.getSandboxMode?.(input);
|
|
103
|
+
const toolNameLine =
|
|
104
|
+
mode === "unsandboxed"
|
|
105
|
+
? `${toolName}${styleText("yellow", " [unsandboxed]")}`
|
|
106
|
+
: toolName;
|
|
72
107
|
return [
|
|
73
|
-
|
|
108
|
+
toolNameLine,
|
|
74
109
|
`command: ${JSON.stringify(execCommandInput.command)}`,
|
|
75
|
-
|
|
110
|
+
formatCommandArgs(execCommandInput.args),
|
|
76
111
|
].join("\n");
|
|
77
112
|
}
|
|
78
113
|
|
|
@@ -117,7 +152,7 @@ export async function formatToolUse(toolUse) {
|
|
|
117
152
|
return [
|
|
118
153
|
`${toolName}`,
|
|
119
154
|
`command: ${tmuxCommandInput.command}`,
|
|
120
|
-
|
|
155
|
+
formatCommandArgs(tmuxCommandInput.args),
|
|
121
156
|
].join("\n");
|
|
122
157
|
}
|
|
123
158
|
|
|
@@ -355,9 +390,10 @@ export function formatCostForBatch(summary) {
|
|
|
355
390
|
/**
|
|
356
391
|
* Print a message to the console.
|
|
357
392
|
* @param {Message} message
|
|
393
|
+
* @param {{ execCommandTool?: SandboxModeProvider }} [options]
|
|
358
394
|
* @returns {Promise<void>}
|
|
359
395
|
*/
|
|
360
|
-
export async function printMessage(message) {
|
|
396
|
+
export async function printMessage(message, options = {}) {
|
|
361
397
|
switch (message.role) {
|
|
362
398
|
case "assistant": {
|
|
363
399
|
// console.log(styleText("bold", "\nAgent:"));
|
|
@@ -366,7 +402,7 @@ export async function printMessage(message) {
|
|
|
366
402
|
(part) => part.type === "tool_use",
|
|
367
403
|
);
|
|
368
404
|
const formattedToolUses = await Promise.all(
|
|
369
|
-
toolUseParts.map((part) => formatToolUse(part)),
|
|
405
|
+
toolUseParts.map((part) => formatToolUse(part, options)),
|
|
370
406
|
);
|
|
371
407
|
let toolUseIndex = 0;
|
|
372
408
|
for (const part of message.content) {
|
|
@@ -385,7 +421,7 @@ export async function printMessage(message) {
|
|
|
385
421
|
// console.log(part.text);
|
|
386
422
|
// break;
|
|
387
423
|
case "tool_use":
|
|
388
|
-
console.log(styleText("bold", "\nTool
|
|
424
|
+
console.log(styleText("bold", "\nTool use:"));
|
|
389
425
|
console.log(formattedToolUses[toolUseIndex++]);
|
|
390
426
|
break;
|
|
391
427
|
}
|
package/src/cli/interactive.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "../agent"
|
|
3
3
|
* @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs"
|
|
4
|
+
* @import { Tool, SandboxModeProvider } from "../tool"
|
|
4
5
|
* @import { VoiceInputConfig } from "../voice/input.mjs"
|
|
5
6
|
* @import { VoiceSession } from "../voice/session.mjs"
|
|
6
7
|
*/
|
|
@@ -67,6 +68,7 @@ const HELP_MESSAGE = [
|
|
|
67
68
|
* @property {() => Promise<void>} onStop
|
|
68
69
|
* @property {ClaudeCodePlugin[]} [claudeCodePlugins]
|
|
69
70
|
* @property {VoiceInputConfig} [voiceInput]
|
|
71
|
+
* @property {Tool & SandboxModeProvider} [execCommandTool]
|
|
70
72
|
*/
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -111,6 +113,7 @@ export function startInteractiveSession({
|
|
|
111
113
|
onStop,
|
|
112
114
|
claudeCodePlugins,
|
|
113
115
|
voiceInput,
|
|
116
|
+
execCommandTool,
|
|
114
117
|
}) {
|
|
115
118
|
/** @type {{ turn: boolean, multiLineBuffer: string[] | null, subagentName: string, toolSpinnerIndex: number, toolSpinnerLastTime: number }} */
|
|
116
119
|
const state = {
|
|
@@ -517,7 +520,7 @@ export function startInteractiveSession({
|
|
|
517
520
|
|
|
518
521
|
agentEventEmitter.on("message", (message) => {
|
|
519
522
|
enqueueOutput(() =>
|
|
520
|
-
printMessage(message).catch((err) => {
|
|
523
|
+
printMessage(message, { execCommandTool }).catch((err) => {
|
|
521
524
|
console.error(
|
|
522
525
|
styleText("red", `Error rendering message: ${err.message}`),
|
|
523
526
|
);
|
package/src/main.mjs
CHANGED
|
@@ -308,8 +308,9 @@ export async function main(argv = process.argv) {
|
|
|
308
308
|
skills: Array.from(prompts.values()).filter((p) => p.isSkill),
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
+
const execCommandTool = createExecCommandTool({ sandbox: appConfig.sandbox });
|
|
311
312
|
const builtinTools = [
|
|
312
|
-
|
|
313
|
+
execCommandTool,
|
|
313
314
|
readFileTool,
|
|
314
315
|
writeFileTool,
|
|
315
316
|
createPatchFileTool(),
|
|
@@ -443,6 +444,7 @@ export async function main(argv = process.argv) {
|
|
|
443
444
|
} else {
|
|
444
445
|
startInteractiveSession({
|
|
445
446
|
...sessionOptions,
|
|
447
|
+
execCommandTool,
|
|
446
448
|
notifyCmd: appConfig.notifyCmd,
|
|
447
449
|
claudeCodePlugins: resolvePluginPaths(appConfig.claudeCodePlugins ?? []),
|
|
448
450
|
voiceInput: appConfig.voiceInput,
|
|
@@ -145,7 +145,7 @@ export async function callAnthropicModel(
|
|
|
145
145
|
|
|
146
146
|
const response = await runFetch();
|
|
147
147
|
|
|
148
|
-
if (response.status === 429 || response.status >= 500) {
|
|
148
|
+
if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
|
|
149
149
|
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
150
150
|
console.error(
|
|
151
151
|
styleText(
|
|
@@ -107,7 +107,7 @@ export async function callBedrockConverseModel(
|
|
|
107
107
|
(response.status === 429 ||
|
|
108
108
|
response.status === 502 ||
|
|
109
109
|
response.status === 503) &&
|
|
110
|
-
retryCount <
|
|
110
|
+
retryCount < 5
|
|
111
111
|
) {
|
|
112
112
|
const retryInterval = Math.min(2 * 2 ** retryCount, 16);
|
|
113
113
|
console.error(
|
package/src/providers/gemini.mjs
CHANGED
|
@@ -145,7 +145,10 @@ export function createCacheEnabledGeminiModelCaller(
|
|
|
145
145
|
signal: AbortSignal.timeout(8 * 60 * 1000),
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
(response.status === 429 || response.status >= 500) &&
|
|
150
|
+
retryCount < 5
|
|
151
|
+
) {
|
|
149
152
|
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
150
153
|
console.error(
|
|
151
154
|
styleText(
|
|
@@ -219,15 +222,20 @@ export function createCacheEnabledGeminiModelCaller(
|
|
|
219
222
|
message instanceof GeminiNoCandidateError ||
|
|
220
223
|
message instanceof GeminiMalformedFunctionCallError
|
|
221
224
|
) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
if (retryCount < 5) {
|
|
226
|
+
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
227
|
+
console.error(
|
|
228
|
+
styleText(
|
|
229
|
+
"yellow",
|
|
230
|
+
`${message.name}: Retrying in ${interval} seconds...`,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
234
|
+
return modelCaller(config, input, retryCount + 1);
|
|
235
|
+
}
|
|
236
|
+
return new Error(
|
|
237
|
+
`${message.name}: Retry limit (${retryCount}) exceeded: ${message.message}`,
|
|
228
238
|
);
|
|
229
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
230
|
-
return modelCaller(config, input, retryCount + 1);
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
// Create context cache for next request
|
package/src/providers/openai.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export async function callOpenAIModel(
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
const retryInterval = Math.min(2 * 2 ** retryCount, 16);
|
|
71
|
-
if (response.status === 429 || response.status >= 500) {
|
|
71
|
+
if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
|
|
72
72
|
console.error(
|
|
73
73
|
styleText(
|
|
74
74
|
"yellow",
|
|
@@ -179,7 +179,7 @@ export async function callOpenAICompatibleModel(
|
|
|
179
179
|
maxAttempt: 5,
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
-
if (response.status === 429 || response.status >= 500) {
|
|
182
|
+
if ((response.status === 429 || response.status >= 500) && retryCount < 5) {
|
|
183
183
|
console.error(
|
|
184
184
|
styleText(
|
|
185
185
|
"yellow",
|
|
@@ -236,18 +236,25 @@ export async function callOpenAICompatibleModel(
|
|
|
236
236
|
|
|
237
237
|
const chatCompletion = convertOpenAIStreamDataToChatCompletion(dataList);
|
|
238
238
|
if (chatCompletion instanceof Error) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
239
|
+
if (retryCount < 5) {
|
|
240
|
+
console.error(
|
|
241
|
+
styleText(
|
|
242
|
+
"yellow",
|
|
243
|
+
`Failed to process stream: ${chatCompletion.message}; Retry in ${retryInterval} seconds...`,
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
await new Promise((resolve) =>
|
|
247
|
+
setTimeout(resolve, retryInterval * 1000),
|
|
248
|
+
);
|
|
249
|
+
return callOpenAICompatibleModel(
|
|
250
|
+
platformConfig,
|
|
251
|
+
modelConfig,
|
|
252
|
+
input,
|
|
253
|
+
retryCount + 1,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Failed to process OpenAI compatible stream after ${retryCount} retries: ${chatCompletion.message}`,
|
|
251
258
|
);
|
|
252
259
|
}
|
|
253
260
|
|
package/src/tool.d.ts
CHANGED
|
@@ -10,6 +10,20 @@ export type Tool = {
|
|
|
10
10
|
injectImpl?: (impl: ToolImplementation) => void;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
export type SandboxMode = "sandbox" | "unsandboxed" | null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Implemented by tools that can report the sandbox mode for a given input.
|
|
17
|
+
* - `null` if the tool has no sandbox configuration
|
|
18
|
+
* - `"unsandboxed"` if a sandbox rule matched with `mode: "unsandboxed"`
|
|
19
|
+
* - `"sandbox"` otherwise (sandbox config exists, default execution is sandboxed)
|
|
20
|
+
* Used by the CLI to display an `[unsandboxed]` badge for tool calls that
|
|
21
|
+
* will execute outside the sandbox.
|
|
22
|
+
*/
|
|
23
|
+
export type SandboxModeProvider = {
|
|
24
|
+
getSandboxMode: (input: unknown) => SandboxMode;
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
export type ToolDefinition = {
|
|
14
28
|
name: string;
|
|
15
29
|
description: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { Tool } from '../tool'
|
|
2
|
+
* @import { Tool, SandboxModeProvider } from '../tool'
|
|
3
3
|
* @import { ExecCommandConfig, ExecCommandInput, ExecCommandSanboxConfig } from './execCommand'
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -13,10 +13,10 @@ const OUTPUT_TRUNCATED_LENGTH = 1024 * 2;
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {ExecCommandConfig=} config
|
|
16
|
-
* @returns {Tool}
|
|
16
|
+
* @returns {Tool & SandboxModeProvider}
|
|
17
17
|
*/
|
|
18
18
|
export function createExecCommandTool(config) {
|
|
19
|
-
/** @type {Tool} */
|
|
19
|
+
/** @type {Tool & SandboxModeProvider} */
|
|
20
20
|
return {
|
|
21
21
|
def: {
|
|
22
22
|
name: "exec_command",
|
|
@@ -190,6 +190,21 @@ Examples:
|
|
|
190
190
|
child.stdin?.end();
|
|
191
191
|
});
|
|
192
192
|
}),
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Report the sandbox mode for a given tool input. Mirrors
|
|
196
|
+
* `rewriteInputForSandbox`'s rule-matching logic so the CLI can preview
|
|
197
|
+
* the mode before execution.
|
|
198
|
+
* @param {unknown} input
|
|
199
|
+
* @returns {"sandbox" | "unsandboxed" | null}
|
|
200
|
+
*/
|
|
201
|
+
getSandboxMode: (input) => {
|
|
202
|
+
if (!config?.sandbox) return null;
|
|
203
|
+
const matchedRule = (config.sandbox.rules || []).find((rule) =>
|
|
204
|
+
matchValue(/** @type {ExecCommandInput} */ (input), rule.pattern),
|
|
205
|
+
);
|
|
206
|
+
return matchedRule?.mode === "unsandboxed" ? "unsandboxed" : "sandbox";
|
|
207
|
+
},
|
|
193
208
|
};
|
|
194
209
|
}
|
|
195
210
|
|