@iinm/plain-agent 1.10.2 → 1.10.3

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.
@@ -1,573 +0,0 @@
1
- /**
2
- * @import { Message, MessageContentToolUse, MessageContentToolResult, ProviderTokenUsage } from "./model"
3
- * @import { CompactContextInput } from "./tools/compactContext"
4
- * @import { ExecCommandInput } from "./tools/execCommand"
5
- * @import { PatchBlock, PatchFileInput } from "./tools/patchFile"
6
- * @import { ReadFileInput } from "./tools/readFile"
7
- * @import { WriteFileInput } from "./tools/writeFile"
8
- * @import { TmuxCommandInput } from "./tools/tmuxCommand"
9
- * @import { SwitchToSubagentInput } from "./tools/switchToSubagent"
10
- */
11
-
12
- import fs from "node:fs/promises";
13
- import { styleText } from "node:util";
14
- import { parseBlocks } from "./tools/patchFile.mjs";
15
- import { diffLines } from "./utils/diffLines.mjs";
16
- import { noThrow } from "./utils/noThrow.mjs";
17
-
18
- /** Length above which a single-line arg forces block-form rendering. */
19
- const ARG_BLOCK_LENGTH_THRESHOLD = 60;
20
-
21
- /**
22
- * Format an args array for display.
23
- * Uses compact JSON for short single-line args; switches to a YAML-style
24
- * block form when any arg contains newlines or exceeds
25
- * {@link ARG_BLOCK_LENGTH_THRESHOLD} characters so that long scripts passed
26
- * to `bash -c`, `python -c`, `node -e`, etc. stay readable.
27
- * @param {unknown} args
28
- * @returns {string}
29
- */
30
- export function formatArgs(args) {
31
- if (!Array.isArray(args) || args.length === 0) {
32
- return `args: ${JSON.stringify(args ?? [])}`;
33
- }
34
-
35
- const needsBlock = args.some(
36
- (a) =>
37
- typeof a === "string" &&
38
- (a.includes("\n") || a.length > ARG_BLOCK_LENGTH_THRESHOLD),
39
- );
40
- if (!needsBlock) {
41
- return `args: ${JSON.stringify(args)}`;
42
- }
43
-
44
- const lines = ["args:"];
45
- for (const arg of args) {
46
- if (
47
- typeof arg === "string" &&
48
- (arg.includes("\n") || arg.length > ARG_BLOCK_LENGTH_THRESHOLD)
49
- ) {
50
- lines.push(" - |");
51
- for (const line of arg.split("\n")) {
52
- lines.push(` ${line}`);
53
- }
54
- } else {
55
- lines.push(` - ${JSON.stringify(arg)}`);
56
- }
57
- }
58
- return lines.join("\n");
59
- }
60
-
61
- /**
62
- * Format tool use for display.
63
- * @param {MessageContentToolUse} toolUse
64
- * @returns {Promise<string>}
65
- */
66
- export async function formatToolUse(toolUse) {
67
- const { toolName, input } = toolUse;
68
-
69
- if (toolName === "exec_command") {
70
- /** @type {Partial<ExecCommandInput>} */
71
- const execCommandInput = input;
72
- return [
73
- `tool: ${toolName}`,
74
- `command: ${JSON.stringify(execCommandInput.command)}`,
75
- formatArgs(execCommandInput.args),
76
- ].join("\n");
77
- }
78
-
79
- if (toolName === "write_file") {
80
- /** @type {Partial<WriteFileInput>} */
81
- const writeFileInput = input;
82
- return [
83
- `tool: ${toolName}`,
84
- `filePath: ${writeFileInput.filePath}`,
85
- `content:\n${writeFileInput.content}`,
86
- ].join("\n");
87
- }
88
-
89
- if (toolName === "patch_file") {
90
- /** @type {Partial<PatchFileInput>} */
91
- const patchFileInput = input;
92
- const filePath = patchFileInput.filePath ?? "";
93
- const patch = patchFileInput.patch || "";
94
- const rendered = await renderPatch(filePath, patch);
95
- return [
96
- `tool: ${toolName}`,
97
- `path: ${filePath}`,
98
- `patch:\n${rendered}`,
99
- ].join("\n");
100
- }
101
-
102
- if (toolName === "read_file") {
103
- /** @type {Partial<ReadFileInput>} */
104
- const readFileInput = input;
105
- /** @type {string[]} */
106
- const lines = [`tool: ${toolName}`, `filePath: ${readFileInput.filePath}`];
107
- if (readFileInput.offset !== undefined) {
108
- lines.push(`offset: ${readFileInput.offset}`);
109
- }
110
- if (readFileInput.limit !== undefined) {
111
- lines.push(`limit: ${readFileInput.limit}`);
112
- }
113
- return lines.join("\n");
114
- }
115
-
116
- if (toolName === "tmux_command") {
117
- /** @type {Partial<TmuxCommandInput>} */
118
- const tmuxCommandInput = input;
119
- return [
120
- `tool: ${toolName}`,
121
- `command: ${tmuxCommandInput.command}`,
122
- formatArgs(tmuxCommandInput.args),
123
- ].join("\n");
124
- }
125
-
126
- if (toolName === "switch_to_subagent") {
127
- /** @type {Partial<SwitchToSubagentInput>} */
128
- const switchToSubagentInput = input;
129
- return [
130
- `tool: ${toolName}`,
131
- `name: ${switchToSubagentInput.name}`,
132
- `goal: ${switchToSubagentInput.goal}`,
133
- ].join("\n");
134
- }
135
-
136
- if (toolName === "compact_context") {
137
- /** @type {Partial<CompactContextInput>} */
138
- const compactContextInput = input;
139
- return [
140
- `tool: ${toolName}`,
141
- `memoryPath: ${compactContextInput.memoryPath}`,
142
- `reason: ${compactContextInput.reason}`,
143
- ].join("\n");
144
- }
145
-
146
- if (toolName === "switch_to_main_agent") {
147
- /** @type {Partial<import("./tools/switchToMainAgent").SwitchToMainAgentInput>} */
148
- const switchToMainAgentInput = input;
149
- return [
150
- `tool: ${toolName}`,
151
- `memoryPath: ${switchToMainAgentInput.memoryPath}`,
152
- ].join("\n");
153
- }
154
-
155
- if (toolName === "web_search") {
156
- /** @type {Partial<import("./tools/webSearch.mjs").WebSearchInput>} */
157
- const webSearchInput = input;
158
- const searchesLine = webSearchInput.searches
159
- ? webSearchInput.searches.map((s) => s.keywords.join(" ")).join(" | ")
160
- : "";
161
- return [
162
- `tool: ${toolName}`,
163
- `searches: ${searchesLine}`,
164
- `question: ${webSearchInput.question}`,
165
- ].join("\n");
166
- }
167
-
168
- if (toolName === "web_fetch") {
169
- /** @type {Partial<import("./tools/webFetch.mjs").WebFetchInput>} */
170
- const webFetchInput = input;
171
- return [
172
- `tool: ${toolName}`,
173
- `url: ${webFetchInput.url}`,
174
- `question: ${webFetchInput.question}`,
175
- ].join("\n");
176
- }
177
-
178
- const { provider: _, ...filteredToolUse } = toolUse;
179
-
180
- return JSON.stringify(filteredToolUse, null, 2);
181
- }
182
-
183
- /** Maximum length of output to display */
184
- const MAX_DISPLAY_OUTPUT_LENGTH = 1024;
185
-
186
- /**
187
- * Format tool result for display.
188
- * @param {MessageContentToolResult} toolResult
189
- * @returns {string}
190
- */
191
- export function formatToolResult(toolResult) {
192
- const { content, isError } = toolResult;
193
-
194
- /** @type {string[]} */
195
- const contentStringParts = [];
196
- for (const part of content) {
197
- switch (part.type) {
198
- case "text":
199
- contentStringParts.push(part.text);
200
- break;
201
- case "image":
202
- contentStringParts.push(
203
- `data:${part.mimeType};base64,${part.data.slice(0, 20)}...`,
204
- );
205
- break;
206
- default:
207
- console.log(`Unsupported content part: ${JSON.stringify(part)}`);
208
- break;
209
- }
210
- }
211
-
212
- const contentString = contentStringParts.join("\n\n");
213
-
214
- if (isError) {
215
- return styleText("red", contentString);
216
- }
217
-
218
- if (toolResult.toolName === "exec_command") {
219
- return contentString
220
- .replace(/(^<stdout>|<\/stdout>$)/gm, styleText("blue", "$1"))
221
- .replace(
222
- /(<truncated_output.+?>|<\/truncated_output>)/g,
223
- styleText("yellow", "$1"),
224
- )
225
- .replace(/(^<stderr>|<\/stderr>$)/gm, styleText("magenta", "$1"))
226
- .replace(/(^<error>|<\/error>$)/gm, styleText("red", "$1"));
227
- }
228
-
229
- if (toolResult.toolName === "read_file") {
230
- return contentString.replace(
231
- /^(\s*\d+:[0-9a-f]{2}\|)/gm,
232
- styleText("gray", "$1"),
233
- );
234
- }
235
-
236
- if (toolResult.toolName === "tmux_command") {
237
- return contentString
238
- .replace(/(^<stdout>|<\/stdout>$)/gm, styleText("blue", "$1"))
239
- .replace(/(^<stderr>|<\/stderr>$)/gm, styleText("magenta", "$1"))
240
- .replace(/(^<error>|<\/error>$)/gm, styleText("red", "$1"))
241
- .replace(/(^<tmux:.*?>|<\/tmux:.*?>$)/gm, styleText("green", "$1"));
242
- }
243
-
244
- if (contentString.length > MAX_DISPLAY_OUTPUT_LENGTH) {
245
- return [
246
- contentString.slice(0, MAX_DISPLAY_OUTPUT_LENGTH),
247
- styleText("yellow", "... (Output truncated for display)"),
248
- "\n",
249
- ].join("");
250
- }
251
-
252
- return contentString;
253
- }
254
-
255
- /**
256
- * Format provider token usage for display.
257
- * @param {ProviderTokenUsage} usage
258
- * @returns {string}
259
- */
260
- export function formatProviderTokenUsage(usage) {
261
- /** @type {string[]} */
262
- const lines = [];
263
- /** @type {string[]} */
264
- const header = [];
265
- for (const [key, value] of Object.entries(usage)) {
266
- if (typeof value === "number") {
267
- header.push(`${key}: ${value}`);
268
- } else if (typeof value === "string") {
269
- header.push(`${key}: ${value}`);
270
- } else if (value) {
271
- lines.push(
272
- `(${key}) ${Object.entries(value)
273
- .filter(
274
- ([k]) =>
275
- ![
276
- // OpenAI
277
- "audio_tokens",
278
- "accepted_prediction_tokens",
279
- "rejected_prediction_tokens",
280
- ].includes(k),
281
- )
282
- .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
283
- .join(", ")}`,
284
- );
285
- }
286
- }
287
-
288
- const outputLines = [`\n${header.join(", ")}`];
289
-
290
- if (lines.length) {
291
- outputLines.push(lines.join(" / "));
292
- }
293
-
294
- return styleText("gray", outputLines.join("\n"));
295
- }
296
-
297
- /**
298
- * Format cost summary for interactive display
299
- * @param {import("./costTracker.mjs").CostSummary} summary
300
- * @returns {string}
301
- */
302
- export function formatCostSummary(summary) {
303
- if (!summary || Object.keys(summary.breakdown).length === 0) {
304
- return styleText("gray", "No token usage recorded yet.");
305
- }
306
-
307
- const lines = [];
308
-
309
- if (summary.totalCost !== undefined) {
310
- lines.push(
311
- styleText(
312
- "bold",
313
- `\nTotal: ${summary.totalCost.toFixed(4)} ${summary.currency}`,
314
- ),
315
- );
316
- } else {
317
- lines.push(styleText("yellow", "Total: N/A (no cost configuration)"));
318
- }
319
-
320
- lines.push(styleText("bold", "\nTokens:"));
321
- for (const [key, { tokens, cost }] of Object.entries(summary.breakdown)) {
322
- const tokenStr = `${key}: ${tokens.toLocaleString()}`;
323
-
324
- if (cost !== undefined) {
325
- const costStr = `${cost.toFixed(4)} ${summary.currency}`;
326
- lines.push(` ${tokenStr.padEnd(30)} ${styleText("cyan", costStr)}`);
327
- } else {
328
- lines.push(` ${tokenStr.padEnd(30)} ${styleText("gray", "N/A")}`);
329
- }
330
- }
331
-
332
- return lines.join("\n");
333
- }
334
-
335
- /**
336
- * Format cost for batch mode JSON output
337
- * @param {import("./costTracker.mjs").CostSummary} summary
338
- */
339
- export function formatCostForBatch(summary) {
340
- if (!summary || Object.keys(summary.breakdown).length === 0) {
341
- return undefined;
342
- }
343
-
344
- return {
345
- total: summary.totalCost,
346
- currency: summary.currency,
347
- unit: summary.unit,
348
- breakdown: Object.fromEntries(
349
- Object.entries(summary.breakdown).map(([key, { tokens, cost }]) => [
350
- key,
351
- { tokens, cost },
352
- ]),
353
- ),
354
- };
355
- }
356
-
357
- /**
358
- * Print a message to the console.
359
- * @param {Message} message
360
- * @returns {Promise<void>}
361
- */
362
- export async function printMessage(message) {
363
- switch (message.role) {
364
- case "assistant": {
365
- // console.log(styleText("bold", "\nAgent:"));
366
- // Pre-format all tool_use parts in parallel to avoid sequential awaits
367
- const toolUseParts = message.content.filter(
368
- (part) => part.type === "tool_use",
369
- );
370
- const formattedToolUses = await Promise.all(
371
- toolUseParts.map((part) => formatToolUse(part)),
372
- );
373
- let toolUseIndex = 0;
374
- for (const part of message.content) {
375
- switch (part.type) {
376
- // Note: Streamで表示するためここでは表示しない
377
- // case "thinking":
378
- // console.log(
379
- // [
380
- // styleText("blue", "<thinking>"),
381
- // part.thinking,
382
- // styleText("blue", "</thinking>\n"),
383
- // ].join("\n"),
384
- // );
385
- // break;
386
- // case "text":
387
- // console.log(part.text);
388
- // break;
389
- case "tool_use":
390
- console.log(styleText("bold", "\nTool call:"));
391
- console.log(formattedToolUses[toolUseIndex++]);
392
- break;
393
- }
394
- }
395
- break;
396
- }
397
- case "user": {
398
- for (const part of message.content) {
399
- switch (part.type) {
400
- case "tool_result": {
401
- console.log(styleText("bold", "\nTool result:"));
402
- console.log(formatToolResult(part));
403
- break;
404
- }
405
- case "text": {
406
- console.log(styleText("bold", "\nUser:"));
407
- const highlighted = part.text.replace(
408
- /^(<context.+?>|<\/context>)/gm,
409
- styleText("green", "$1"),
410
- );
411
- console.log(highlighted);
412
- break;
413
- }
414
- case "image": {
415
- break;
416
- }
417
- default: {
418
- console.log(styleText("bold", "\nUnknown Message Format:"));
419
- console.log(JSON.stringify(part, null, 2));
420
- }
421
- }
422
- }
423
- break;
424
- }
425
- default: {
426
- console.log(styleText("bold", "\nUnknown Message Format:"));
427
- console.log(JSON.stringify(message, null, 2));
428
- }
429
- }
430
- }
431
-
432
- /**
433
- * Render a patch_file `patch` string for terminal display.
434
- *
435
- * Attempts to show a side-by-side diff (- removed, + added, unchanged)
436
- * by parsing the patch and reading the target file. Falls back to plain
437
- * syntax highlighting on any failure.
438
- *
439
- * @param {string} filePath
440
- * @param {string} patch
441
- * @returns {Promise<string>}
442
- */
443
- async function renderPatch(filePath, patch) {
444
- if (!patch) {
445
- return "";
446
- }
447
- const fallback = highlightPatchPlain(patch);
448
-
449
- const nonce = extractPatchNonce(patch);
450
- if (!nonce) {
451
- return fallback;
452
- }
453
-
454
- /** @type {PatchBlock[]} */
455
- let blocks;
456
- try {
457
- blocks = parseBlocks(patch, nonce);
458
- } catch {
459
- return fallback;
460
- }
461
-
462
- let originalLines = null;
463
- if (filePath) {
464
- const original = await noThrow(() => fs.readFile(filePath, "utf8"));
465
- if (!(original instanceof Error)) {
466
- originalLines = splitContentLines(original);
467
- }
468
- }
469
-
470
- return blocks
471
- .map((block) => renderPatchBlock(block, originalLines, nonce))
472
- .join("\n\n");
473
- }
474
-
475
- /**
476
- * @param {PatchBlock} block
477
- * @param {string[] | null} originalLines
478
- * @param {string} nonce
479
- * @returns {string}
480
- */
481
- function renderPatchBlock(block, originalLines, nonce) {
482
- /** @type {string[]} */
483
- const out = [];
484
- if (block.op === "replace") {
485
- out.push(
486
- styleText(
487
- "cyan",
488
- `@@@ ${nonce} ${block.start}:${block.startHash}-${block.end}:${block.endHash}`,
489
- ),
490
- );
491
- if (originalLines) {
492
- const safeStart = Math.max(1, block.start);
493
- const safeEnd = Math.min(originalLines.length, block.end);
494
- const oldSlice = originalLines.slice(safeStart - 1, safeEnd);
495
- // Use a real line diff so unchanged lines render as context
496
- // (no color, " " prefix) instead of being shown as both "- " and
497
- // "+ ".
498
- for (const op of diffLines(oldSlice, block.body)) {
499
- if (op.type === "-") {
500
- out.push(styleText("red", `- ${op.line}`));
501
- } else if (op.type === "+") {
502
- out.push(styleText("green", `+ ${op.line}`));
503
- } else {
504
- out.push(` ${op.line}`);
505
- }
506
- }
507
- } else {
508
- // No file context available — fall back to listing the body as
509
- // additions so the user can still see the new content.
510
- for (const line of block.body) {
511
- out.push(styleText("green", `+ ${line}`));
512
- }
513
- }
514
- } else {
515
- const afterSuffix = block.afterHash ? `:${block.afterHash}` : "";
516
- out.push(styleText("cyan", `@@@ ${nonce} ${block.after}${afterSuffix}+`));
517
- for (const line of block.body) {
518
- out.push(styleText("green", `+ ${line}`));
519
- }
520
- }
521
- out.push(styleText("cyan", `@@@ ${nonce}`));
522
- return out.join("\n");
523
- }
524
-
525
- /**
526
- * Verbatim highlighter used as fallback when block-aware rendering is not
527
- * possible (parse error, missing nonce, etc.).
528
- * @param {string} patch
529
- * @returns {string}
530
- */
531
- function highlightPatchPlain(patch) {
532
- if (!patch) {
533
- return "";
534
- }
535
- // Patch headers/closes look like "@@@ <nonce> ..." or "@@@ <nonce>".
536
- const headerRegex = /^@@@\s+\S+(\s.*)?$/;
537
- return patch
538
- .split("\n")
539
- .map((line) => {
540
- if (headerRegex.test(line)) {
541
- return styleText("cyan", line);
542
- }
543
- if (line === "") {
544
- return line;
545
- }
546
- return styleText("green", line);
547
- })
548
- .join("\n");
549
- }
550
-
551
- /**
552
- * Extract the nonce from the first open marker in a patch_file patch.
553
- * @param {string} patch
554
- * @returns {string | null}
555
- */
556
- function extractPatchNonce(patch) {
557
- const match = patch.match(/^@@@\s+(\S+)/m);
558
- return match ? match[1] : null;
559
- }
560
-
561
- /**
562
- * Split file content into lines, dropping the trailing empty element when
563
- * the file ends with a newline (matches patch_file's own line indexing).
564
- * @param {string} content
565
- * @returns {string[]}
566
- */
567
- function splitContentLines(content) {
568
- const lines = content.split("\n");
569
- if (lines.length > 0 && lines[lines.length - 1] === "") {
570
- lines.pop();
571
- }
572
- return lines;
573
- }
@@ -1,61 +0,0 @@
1
- import { startGeminiVoiceSession } from "./voiceInputGemini.mjs";
2
- import { startOpenAIVoiceSession } from "./voiceInputOpenAI.mjs";
3
- import { failVoiceSessionAsync } from "./voiceInputSession.mjs";
4
-
5
- export {
6
- createCJKSpaceNormalizer,
7
- detectRecorder,
8
- getRecorderCandidates,
9
- } from "./voiceInputSession.mjs";
10
- export { parseVoiceToggleKey } from "./voiceToggleKey.mjs";
11
-
12
- /**
13
- * @typedef {import("./voiceInputSession.mjs").VoiceRecorderConfig} VoiceRecorderConfig
14
- */
15
-
16
- /**
17
- * @typedef {import("./voiceInputSession.mjs").VoiceSessionCallbacks} VoiceSessionCallbacks
18
- */
19
-
20
- /**
21
- * @typedef {import("./voiceInputSession.mjs").VoiceSession} VoiceSession
22
- */
23
-
24
- /**
25
- * @typedef {import("./voiceToggleKey.mjs").VoiceToggleKey} VoiceToggleKey
26
- */
27
-
28
- /**
29
- * @typedef {import("./voiceInputOpenAI.mjs").VoiceInputOpenAIConfig} VoiceInputOpenAIConfig
30
- */
31
-
32
- /**
33
- * @typedef {import("./voiceInputGemini.mjs").VoiceInputGeminiConfig} VoiceInputGeminiConfig
34
- */
35
-
36
- /**
37
- * @typedef {VoiceInputOpenAIConfig | VoiceInputGeminiConfig} VoiceInputConfig
38
- */
39
-
40
- /**
41
- * Start a voice input session. Dispatches to the provider-specific
42
- * implementation based on `config.provider`.
43
- *
44
- * @param {object} options
45
- * @param {VoiceInputConfig} options.config
46
- * @param {VoiceSessionCallbacks} options.callbacks
47
- * @returns {VoiceSession}
48
- */
49
- export function startVoiceSession({ config, callbacks }) {
50
- if (config.provider === "openai") {
51
- return startOpenAIVoiceSession({ config, callbacks });
52
- }
53
- if (config.provider === "gemini") {
54
- return startGeminiVoiceSession({ config, callbacks });
55
- }
56
- const provider = /** @type {{ provider: string }} */ (config).provider;
57
- return failVoiceSessionAsync(
58
- callbacks,
59
- new Error(`Unsupported voiceInput.provider: ${provider}`),
60
- );
61
- }
File without changes
File without changes
File without changes