@iinm/plain-agent 1.6.1 → 1.7.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/README.md +6 -8
- package/package.json +1 -1
- package/src/agent.d.ts +1 -0
- package/src/agent.mjs +14 -0
- package/src/agentLoop.mjs +12 -9
- package/src/cliArgs.mjs +5 -5
- package/src/cliCompleter.mjs +2 -2
- package/src/cliInteractive.mjs +14 -4
- package/src/cliPasteTransform.mjs +3 -4
- package/src/env.mjs +0 -5
- package/src/subagent.mjs +7 -3
- package/bin/plain-interrupt +0 -6
- package/src/context/consumeInterruptMessage.mjs +0 -30
package/README.md
CHANGED
|
@@ -238,9 +238,10 @@ Run in batch mode (non-interactive).
|
|
|
238
238
|
In batch mode, config files are not loaded automatically. Only the files specified with `--config` are loaded.
|
|
239
239
|
|
|
240
240
|
```sh
|
|
241
|
-
plain batch
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
plain batch \
|
|
242
|
+
-c ~/.config/plain-agent/config.local.json \
|
|
243
|
+
-c .plain-agent/config.json \
|
|
244
|
+
"Add tests for ..."
|
|
244
245
|
```
|
|
245
246
|
|
|
246
247
|
Display the help message.
|
|
@@ -249,11 +250,9 @@ Display the help message.
|
|
|
249
250
|
/help
|
|
250
251
|
```
|
|
251
252
|
|
|
252
|
-
Interrupt the agent while it's running
|
|
253
|
+
Interrupt the agent while it's running:
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
plain-interrupt Stop and report the progress
|
|
256
|
-
```
|
|
255
|
+
Press **Ctrl-C** to pause auto-approve. The agent will finish the current tool call, then return to the prompt.
|
|
257
256
|
|
|
258
257
|
## Available Tools
|
|
259
258
|
|
|
@@ -281,7 +280,6 @@ The agent can use the following tools to assist with tasks:
|
|
|
281
280
|
\__ .plain-agent/
|
|
282
281
|
\__ config.json # Project-specific configuration
|
|
283
282
|
\__ config.local.json # Project-specific local configuration (including secrets)
|
|
284
|
-
\__ interrupt-message.txt # Interrupt message consumed by the agent
|
|
285
283
|
\__ memory/ # Task-specific memory files
|
|
286
284
|
\__ prompts/ # Project-specific prompts
|
|
287
285
|
\__ agents/ # Project-specific agent roles
|
package/package.json
CHANGED
package/src/agent.d.ts
CHANGED
package/src/agent.mjs
CHANGED
|
@@ -144,6 +144,16 @@ export function createAgent({
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// Pause signal: set by Ctrl-C during agent execution, checked after each tool batch completes
|
|
148
|
+
let paused = false;
|
|
149
|
+
/** @type {import("./agentLoop.mjs").PauseSignal} */
|
|
150
|
+
const pauseSignal = {
|
|
151
|
+
isPaused: () => paused,
|
|
152
|
+
reset: () => {
|
|
153
|
+
paused = false;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
147
157
|
const agentLoop = createAgentLoop({
|
|
148
158
|
callModel,
|
|
149
159
|
stateManager,
|
|
@@ -152,6 +162,7 @@ export function createAgent({
|
|
|
152
162
|
agentEventEmitter,
|
|
153
163
|
toolUseApprover,
|
|
154
164
|
subagentManager,
|
|
165
|
+
pauseSignal,
|
|
155
166
|
});
|
|
156
167
|
|
|
157
168
|
userEventEmitter.on("userInput", agentLoop.handleUserInput);
|
|
@@ -163,6 +174,9 @@ export function createAgent({
|
|
|
163
174
|
dumpMessages,
|
|
164
175
|
loadMessages,
|
|
165
176
|
getCostSummary: () => costTracker.calculateCost(),
|
|
177
|
+
pauseAutoApprove: () => {
|
|
178
|
+
paused = true;
|
|
179
|
+
},
|
|
166
180
|
},
|
|
167
181
|
};
|
|
168
182
|
}
|
package/src/agentLoop.mjs
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { styleText } from "node:util";
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} PauseSignal
|
|
13
|
+
* @property {() => boolean} isPaused - Returns true if auto-approve should be paused
|
|
14
|
+
* @property {() => void} reset - Resets the paused state
|
|
15
|
+
*/
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* @typedef {Object} AgentLoopConfig
|
|
@@ -18,6 +23,7 @@ import { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
|
|
|
18
23
|
* @property {AgentEventEmitter} agentEventEmitter - Event emitter for agent events
|
|
19
24
|
* @property {ToolUseApprover} toolUseApprover - Tool use approval checker
|
|
20
25
|
* @property {SubagentManager} subagentManager - Subagent manager instance
|
|
26
|
+
* @property {PauseSignal} pauseSignal - Signal to pause auto-approve after current tool completes
|
|
21
27
|
*/
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -36,6 +42,7 @@ export function createAgentLoop({
|
|
|
36
42
|
agentEventEmitter,
|
|
37
43
|
toolUseApprover,
|
|
38
44
|
subagentManager,
|
|
45
|
+
pauseSignal,
|
|
39
46
|
}) {
|
|
40
47
|
const inputHandler = createInputHandler({
|
|
41
48
|
stateManager,
|
|
@@ -198,14 +205,10 @@ export function createAgentLoop({
|
|
|
198
205
|
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
199
206
|
}
|
|
200
207
|
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
role: "user",
|
|
206
|
-
content: [{ type: "text", text: interruptMessage }],
|
|
207
|
-
},
|
|
208
|
-
]);
|
|
208
|
+
// Check if auto-approve was paused by Ctrl-C during tool execution
|
|
209
|
+
if (pauseSignal.isPaused()) {
|
|
210
|
+
pauseSignal.reset();
|
|
211
|
+
break;
|
|
209
212
|
}
|
|
210
213
|
}
|
|
211
214
|
}
|
package/src/cliArgs.mjs
CHANGED
|
@@ -125,10 +125,12 @@ Usage: plain [options]
|
|
|
125
125
|
Options:
|
|
126
126
|
-m, --model <model+variant> Model to use
|
|
127
127
|
-h, --help Show this help message
|
|
128
|
-
-c, --config <file> Config file to load
|
|
128
|
+
-c, --config <file> Config file to load (repeatable)
|
|
129
129
|
|
|
130
130
|
Subcommands:
|
|
131
|
-
batch <task> Run in batch mode with the given task instruction
|
|
131
|
+
batch <task> Run in batch mode with the given task instruction.
|
|
132
|
+
Config files are NOT auto-loaded in batch mode;
|
|
133
|
+
use -c to specify config files explicitly.
|
|
132
134
|
list-models List available models
|
|
133
135
|
install-claude-code-plugins Install Claude Code plugins
|
|
134
136
|
|
|
@@ -137,9 +139,7 @@ Examples:
|
|
|
137
139
|
plain batch \\
|
|
138
140
|
-c ~/.config/plain-agent/config.local.json \\
|
|
139
141
|
-c .plain-agent/config.json \\
|
|
140
|
-
"Add tests for
|
|
141
|
-
plain list-models
|
|
142
|
-
plain install-claude-code-plugins
|
|
142
|
+
"Add tests for ..."
|
|
143
143
|
`);
|
|
144
144
|
process.exit(exitCode);
|
|
145
145
|
}
|
package/src/cliCompleter.mjs
CHANGED
|
@@ -200,8 +200,8 @@ export function createCompleter(getCliRef, claudeCodePlugins) {
|
|
|
200
200
|
const name = typeof cmd === "string" ? cmd : cmd.name;
|
|
201
201
|
return (
|
|
202
202
|
name !== "/<id>" &&
|
|
203
|
-
(name === "/agents:" || !name.startsWith("/
|
|
204
|
-
(name === "/prompts:" || !name.startsWith("/
|
|
203
|
+
(name === "/agents:" || !name.startsWith("/agents:")) &&
|
|
204
|
+
(name === "/prompts:" || !name.startsWith("/prompts:"))
|
|
205
205
|
);
|
|
206
206
|
},
|
|
207
207
|
);
|
package/src/cliInteractive.mjs
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
createPasteTransform,
|
|
17
17
|
resolvePastePlaceholders,
|
|
18
18
|
} from "./cliPasteTransform.mjs";
|
|
19
|
-
import { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
|
|
20
19
|
import { notify } from "./utils/notify.mjs";
|
|
21
20
|
|
|
22
21
|
const HELP_MESSAGE = [
|
|
@@ -121,7 +120,19 @@ export function startInteractiveSession({
|
|
|
121
120
|
let lastExitAttempt = 0;
|
|
122
121
|
const EXIT_CONFIRM_TIMEOUT = 1500;
|
|
123
122
|
|
|
124
|
-
const
|
|
123
|
+
const handleCtrlC = () => {
|
|
124
|
+
// If agent is running, pause auto-approve instead of exiting
|
|
125
|
+
if (!state.turn) {
|
|
126
|
+
agentCommands.pauseAutoApprove();
|
|
127
|
+
console.log(
|
|
128
|
+
styleText(
|
|
129
|
+
"yellow",
|
|
130
|
+
"\n⚠ Ctrl-C: Auto-approve paused. Finishing current tool...",
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
const now = Date.now();
|
|
126
137
|
if (now - lastExitAttempt < EXIT_CONFIRM_TIMEOUT) {
|
|
127
138
|
handleExit();
|
|
@@ -132,7 +143,7 @@ export function startInteractiveSession({
|
|
|
132
143
|
};
|
|
133
144
|
|
|
134
145
|
// Create a transform stream to handle bracketed paste before readline
|
|
135
|
-
const pasteTransform = createPasteTransform(
|
|
146
|
+
const pasteTransform = createPasteTransform(handleCtrlC);
|
|
136
147
|
|
|
137
148
|
// Set up transformed stdin for readline
|
|
138
149
|
process.stdin.pipe(pasteTransform);
|
|
@@ -198,7 +209,6 @@ export function startInteractiveSession({
|
|
|
198
209
|
}
|
|
199
210
|
|
|
200
211
|
cli.setPrompt(currentCliPrompt);
|
|
201
|
-
await consumeInterruptMessage();
|
|
202
212
|
|
|
203
213
|
const result = await handleCommand(inputTrimmed);
|
|
204
214
|
if (result === "prompt") {
|
|
@@ -53,10 +53,10 @@ export function resolvePastePlaceholders(input) {
|
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Create a Transform stream to handle bracketed paste before readline.
|
|
56
|
-
* @param {() => void}
|
|
56
|
+
* @param {() => void} onCtrlC - Called when Ctrl-C or Ctrl-D is detected
|
|
57
57
|
* @returns {Transform}
|
|
58
58
|
*/
|
|
59
|
-
export function createPasteTransform(
|
|
59
|
+
export function createPasteTransform(onCtrlC) {
|
|
60
60
|
let inPasteMode = false;
|
|
61
61
|
let pasteBuffer = "";
|
|
62
62
|
|
|
@@ -67,8 +67,7 @@ export function createPasteTransform(onExitRequest) {
|
|
|
67
67
|
|
|
68
68
|
// Handle Ctrl-C and Ctrl-D
|
|
69
69
|
if (data.includes("\x03") || data.includes("\x04")) {
|
|
70
|
-
|
|
71
|
-
onExitRequest();
|
|
70
|
+
onCtrlC();
|
|
72
71
|
callback();
|
|
73
72
|
return;
|
|
74
73
|
}
|
package/src/env.mjs
CHANGED
|
@@ -38,9 +38,4 @@ export const AGENT_NOTIFY_CMD_DEFAULT = path.join(
|
|
|
38
38
|
"plain-notify-terminal-bell",
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
-
export const AGENT_INTERRUPT_MESSAGE_FILE_PATH = path.join(
|
|
42
|
-
AGENT_PROJECT_METADATA_DIR,
|
|
43
|
-
"interrupt-message.txt",
|
|
44
|
-
);
|
|
45
|
-
|
|
46
41
|
export const USER_NAME = process.env.USER || "unknown";
|
package/src/subagent.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { reportAsSubagentToolName } from "./tools/reportAsSubagent.mjs";
|
|
|
25
25
|
export function createSubagentManager(agentRoles, handlers) {
|
|
26
26
|
/** @type {{name: string; goal: string; delegationMessageIndex: number}[]} */
|
|
27
27
|
const subagents = [];
|
|
28
|
+
let subagentCount = 0;
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* @typedef {DelegateSuccess | DelegateFailure} DelegateResult
|
|
@@ -79,6 +80,9 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
79
80
|
: role.content;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
subagentCount++;
|
|
84
|
+
const sequenceNumber = String(subagentCount).padStart(2, "0");
|
|
85
|
+
|
|
82
86
|
subagents.push({
|
|
83
87
|
name: actualName,
|
|
84
88
|
goal,
|
|
@@ -89,11 +93,11 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
89
93
|
return {
|
|
90
94
|
success: true,
|
|
91
95
|
value: [
|
|
92
|
-
|
|
96
|
+
`You are now the subagent "${actualName}". Start working on the following goal.`,
|
|
93
97
|
`Your goal: ${goal}`,
|
|
94
98
|
`Role: ${actualName}\n---\n${roleContent}\n---`,
|
|
95
|
-
`Memory file path format: ${AGENT_PROJECT_METADATA_DIR}/memory/<session-id>--${actualName}--<kebab-case-title>.md (Replace <kebab-case-title>
|
|
96
|
-
`
|
|
99
|
+
`Memory file path format: ${AGENT_PROJECT_METADATA_DIR}/memory/<session-id>--${sequenceNumber}--${actualName}--<kebab-case-title>.md (Replace <kebab-case-title> with a short title describing your own goal)`,
|
|
100
|
+
`When finished, call "report_as_subagent" with the memory file path.`,
|
|
97
101
|
].join("\n\n"),
|
|
98
102
|
};
|
|
99
103
|
}
|
package/bin/plain-interrupt
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import { AGENT_INTERRUPT_MESSAGE_FILE_PATH } from "../env.mjs";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @returns {Promise<string | undefined>}
|
|
6
|
-
*/
|
|
7
|
-
export async function consumeInterruptMessage() {
|
|
8
|
-
try {
|
|
9
|
-
const content = await fs.readFile(
|
|
10
|
-
AGENT_INTERRUPT_MESSAGE_FILE_PATH,
|
|
11
|
-
"utf8",
|
|
12
|
-
);
|
|
13
|
-
await fs.truncate(AGENT_INTERRUPT_MESSAGE_FILE_PATH, 0);
|
|
14
|
-
|
|
15
|
-
if (content.trim() === "") {
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
return content;
|
|
19
|
-
} catch (err) {
|
|
20
|
-
if (
|
|
21
|
-
err instanceof Error &&
|
|
22
|
-
"code" in err &&
|
|
23
|
-
typeof err.code === "string" &&
|
|
24
|
-
err.code === "ENOENT"
|
|
25
|
-
) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
throw err;
|
|
29
|
-
}
|
|
30
|
-
}
|