@polylogicai/polycode 1.1.0 → 1.1.2
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/bin/polycode.mjs +80 -16
- package/lib/agentic.mjs +50 -11
- package/lib/compiler.mjs +76 -0
- package/lib/repl-ui.mjs +71 -7
- package/package.json +1 -1
package/bin/polycode.mjs
CHANGED
|
@@ -15,10 +15,15 @@ import { computeAgencyReceipt, formatReceipt } from '../lib/agency-receipt.mjs';
|
|
|
15
15
|
import { fireHook } from '../lib/hooks.mjs';
|
|
16
16
|
import { compilePacket } from '../lib/compiler.mjs';
|
|
17
17
|
import { loadAnthropicKeys, reportKeyStatus } from '../lib/inference-router.mjs';
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
-
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { join, dirname, resolve } from 'node:path';
|
|
21
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = dirname(__filename);
|
|
26
|
+
const PACKAGE_JSON = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
22
27
|
import * as readline from 'node:readline/promises';
|
|
23
28
|
import { stdin, stdout, exit, argv, env, cwd as getCwd } from 'node:process';
|
|
24
29
|
import 'dotenv/config';
|
|
@@ -137,7 +142,7 @@ function loadRules() {
|
|
|
137
142
|
return {};
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
const VERSION =
|
|
145
|
+
const VERSION = PACKAGE_JSON.version;
|
|
141
146
|
const DOCS_URL = 'https://polylogicai.com/polycode';
|
|
142
147
|
|
|
143
148
|
const BANNER = `${C.bold}${C.amber}polycode v${VERSION}${C.reset}
|
|
@@ -173,7 +178,57 @@ function resolveConfigDir() {
|
|
|
173
178
|
return dir;
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
|
|
181
|
+
// Per-working-directory session log scoping. Running polycode from different
|
|
182
|
+
// directories produces different session logs, so history from one project
|
|
183
|
+
// does not contaminate another. The cwd is hashed and the short prefix used
|
|
184
|
+
// as a directory name under ~/.polycode/canon. A cwd.txt file in the scope
|
|
185
|
+
// directory preserves the original path for debugging.
|
|
186
|
+
function resolveScopedCanonFile(configDir, cwd) {
|
|
187
|
+
const cwdHash = createHash('sha256').update(cwd).digest('hex').slice(0, 12);
|
|
188
|
+
const scopeDir = join(configDir, 'canon', cwdHash);
|
|
189
|
+
if (!existsSync(scopeDir)) mkdirSync(scopeDir, { recursive: true });
|
|
190
|
+
const cwdPointerPath = join(scopeDir, 'cwd.txt');
|
|
191
|
+
if (!existsSync(cwdPointerPath)) {
|
|
192
|
+
try { writeFileSync(cwdPointerPath, cwd + '\n'); } catch {}
|
|
193
|
+
}
|
|
194
|
+
return join(scopeDir, `${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// POLYCODE.md project context. On startup, walk up from cwd looking for a
|
|
198
|
+
// project-context file. If found, its content is injected into the agent's
|
|
199
|
+
// system prompt as project context. Supported names in priority order.
|
|
200
|
+
function findPolycodeMd(startDir) {
|
|
201
|
+
const NAMES = ['POLYCODE.md', 'polycode.md', '.polycode.md'];
|
|
202
|
+
let dir = resolve(startDir);
|
|
203
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
204
|
+
for (const name of NAMES) {
|
|
205
|
+
const candidate = join(dir, name);
|
|
206
|
+
try {
|
|
207
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
208
|
+
} catch {
|
|
209
|
+
// not present, try next
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const parent = dirname(dir);
|
|
213
|
+
if (parent === dir) break;
|
|
214
|
+
dir = parent;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function loadProjectContext(cwd) {
|
|
220
|
+
const path = findPolycodeMd(cwd);
|
|
221
|
+
if (!path) return { content: null, path: null };
|
|
222
|
+
try {
|
|
223
|
+
const content = readFileSync(path, 'utf8').slice(0, 8000);
|
|
224
|
+
return { content, path };
|
|
225
|
+
} catch {
|
|
226
|
+
return { content: null, path: null };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function runOneShot(message, opts) {
|
|
231
|
+
const { loop, canon, cwd, renderer, state, sessionId, hookDir } = opts;
|
|
177
232
|
await fireHook('UserPromptSubmit', {
|
|
178
233
|
session_id: sessionId, cwd, hook_event_name: 'UserPromptSubmit', prompt: message,
|
|
179
234
|
}, hookDir);
|
|
@@ -188,12 +243,14 @@ async function runOneShot(message, { loop, canon, cwd, renderer, state, sessionI
|
|
|
188
243
|
state.lastTurnTokens = result.promptTokensUsed;
|
|
189
244
|
state.lastCompiler = result.compilerProvider;
|
|
190
245
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
246
|
+
if (opts.verbose) {
|
|
247
|
+
const receipt = computeAgencyReceipt({
|
|
248
|
+
primitivesList: result.primitivesList,
|
|
249
|
+
wallClockMs: result.durationMs,
|
|
250
|
+
iterations: result.iterations,
|
|
251
|
+
});
|
|
252
|
+
stdout.write(`${C.dim}${formatReceipt(receipt)} . ${canon.size()} rows in log${C.reset}\n`);
|
|
253
|
+
}
|
|
197
254
|
|
|
198
255
|
await fireHook('Stop', { session_id: sessionId, cwd, hook_event_name: 'Stop' }, hookDir);
|
|
199
256
|
return result;
|
|
@@ -202,7 +259,12 @@ async function runOneShot(message, { loop, canon, cwd, renderer, state, sessionI
|
|
|
202
259
|
async function runRepl(opts) {
|
|
203
260
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
204
261
|
stdout.write(BANNER + '\n');
|
|
205
|
-
|
|
262
|
+
if (opts.projectContextPath) {
|
|
263
|
+
stdout.write(`${C.dim}project context: ${opts.projectContextPath}${C.reset}\n`);
|
|
264
|
+
}
|
|
265
|
+
if (opts.canon.size() > 0) {
|
|
266
|
+
stdout.write(`${C.dim}session log: ${opts.canon.size()} rows${C.reset}\n`);
|
|
267
|
+
}
|
|
206
268
|
stdout.write(`${C.dim}type /help for commands. ctrl+c or /exit to leave.${C.reset}\n\n`);
|
|
207
269
|
|
|
208
270
|
try {
|
|
@@ -246,9 +308,11 @@ async function main() {
|
|
|
246
308
|
const model = env.POLYCODE_MODEL || 'moonshotai/kimi-k2-instruct';
|
|
247
309
|
const cwd = env.POLYCODE_CWD || getCwd();
|
|
248
310
|
const hookDir = env.POLYCODE_HOOK_DIR || join(configDir, 'hooks');
|
|
249
|
-
const canonFile = env.POLYCODE_CANON_FILE ||
|
|
311
|
+
const canonFile = env.POLYCODE_CANON_FILE || resolveScopedCanonFile(configDir, cwd);
|
|
312
|
+
const verbose = args.includes('--verbose') || args.includes('-V');
|
|
250
313
|
|
|
251
314
|
const canon = createCanon(canonFile);
|
|
315
|
+
const { content: projectContext, path: projectContextPath } = loadProjectContext(cwd);
|
|
252
316
|
|
|
253
317
|
if (args.includes('--history') || args.includes('--log')) {
|
|
254
318
|
stdout.write(`session log: ${canonFile}\nrows: ${canon.size()}\nlast_hash: ${canon.lastHash()}\n`);
|
|
@@ -292,10 +356,10 @@ async function main() {
|
|
|
292
356
|
canon_path: canonFile,
|
|
293
357
|
}, hookDir);
|
|
294
358
|
|
|
295
|
-
const loop = new AgenticLoop({ apiKey, model, rules });
|
|
296
|
-
const renderer = createRenderer(stdout);
|
|
359
|
+
const loop = new AgenticLoop({ apiKey, model, rules, projectContext });
|
|
360
|
+
const renderer = createRenderer(stdout, { verbose });
|
|
297
361
|
const state = { lastTurnTokens: 0, lastCompiler: null };
|
|
298
|
-
const opts = { loop, canon, cwd, renderer, state, sessionId, hookDir };
|
|
362
|
+
const opts = { loop, canon, cwd, renderer, state, sessionId, hookDir, verbose, projectContextPath };
|
|
299
363
|
|
|
300
364
|
const positional = args.filter((a) => !a.startsWith('--') && args[args.indexOf(a) - 1] !== '--packet');
|
|
301
365
|
if (positional.length > 0) {
|
package/lib/agentic.mjs
CHANGED
|
@@ -23,22 +23,60 @@ const execAsync = promisify(exec);
|
|
|
23
23
|
|
|
24
24
|
const DEFAULT_MODEL = 'moonshotai/kimi-k2-instruct';
|
|
25
25
|
const FALLBACK_MODEL = 'llama-3.3-70b-versatile';
|
|
26
|
-
const DEFAULT_MAX_ITERATIONS =
|
|
26
|
+
const DEFAULT_MAX_ITERATIONS = 8;
|
|
27
27
|
const TEMPERATURE = 0.2;
|
|
28
28
|
const MAX_TOKENS = 4096;
|
|
29
29
|
const MAX_BASH_TIMEOUT_MS = 30_000;
|
|
30
30
|
const MAX_OUTPUT_BYTES = 3200;
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const SYSTEM_PROMPT_BASE = `You are polycode, a coding assistant that runs in the user's terminal. You help users in three distinct modes. Pick the right mode based on the current user message.
|
|
33
|
+
|
|
34
|
+
Mode 1: CONVERSATIONAL. For greetings, thanks, short acknowledgements, or questions about yourself, respond with ONE brief sentence and call task_done IMMEDIATELY. Do not use any tools.
|
|
35
|
+
Examples:
|
|
36
|
+
User: hello
|
|
37
|
+
You: Hi. I am polycode. What can I help you with?
|
|
38
|
+
(call task_done with exactly that text)
|
|
39
|
+
|
|
40
|
+
User: thanks
|
|
41
|
+
You: You're welcome.
|
|
42
|
+
(call task_done with exactly that text)
|
|
43
|
+
|
|
44
|
+
User: who are you
|
|
45
|
+
You: I am polycode, a coding assistant that runs on your machine with your API keys.
|
|
46
|
+
(call task_done with exactly that text)
|
|
47
|
+
|
|
48
|
+
Mode 2: KNOWLEDGE QUESTION. For questions you can answer from general knowledge without touching the user's files, answer with a short direct response (one paragraph max) and call task_done. Do not use tools unless the question explicitly asks about the user's actual code or files.
|
|
49
|
+
Example:
|
|
50
|
+
User: what is the difference between let and const in javascript?
|
|
51
|
+
You: let allows reassignment, const does not. Both are block-scoped. const still allows mutation of object contents.
|
|
52
|
+
(call task_done with that explanation)
|
|
53
|
+
|
|
54
|
+
Mode 3: CODE TASK. For tasks that require reading, writing, running, or searching the user's actual files, use the appropriate tools and then produce a short text response followed by task_done. Available tools: bash, read_file, write_file, edit_file, glob, grep.
|
|
55
|
+
Example:
|
|
56
|
+
User: read package.json and tell me the main entry
|
|
57
|
+
You: (call read_file on package.json, then respond with the answer, then task_done)
|
|
58
|
+
|
|
59
|
+
Hard rules:
|
|
60
|
+
- Always produce a text message in your response. Never call task_done without first saying something to the user.
|
|
61
|
+
- Never loop more than 3 iterations without producing text. If you are not sure what to do, ask the user and call task_done.
|
|
62
|
+
- Do not explore the filesystem unless the user's current message explicitly asks about their files.
|
|
63
|
+
- Do not assume there is an ongoing task from prior turns unless the current message continues it.
|
|
64
|
+
- If a tool call fails, acknowledge the failure in your text response, do not retry the same operation.
|
|
65
|
+
- Use periods, commas, colons. Not em dashes. No hype words.
|
|
66
|
+
|
|
67
|
+
Verification: every tool call is checked by a deterministic layer before it lands in the session log. Failures are marked REFUTED. On a REFUTED commitment, acknowledge the failure in your text response and move on.`;
|
|
68
|
+
|
|
69
|
+
function buildSystemPrompt(projectContext) {
|
|
70
|
+
if (!projectContext || typeof projectContext !== 'string' || !projectContext.trim()) {
|
|
71
|
+
return SYSTEM_PROMPT_BASE;
|
|
72
|
+
}
|
|
73
|
+
return `${SYSTEM_PROMPT_BASE}
|
|
33
74
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- No hype words: no "revolutionary", "game-changer", "unprecedented".
|
|
37
|
-
- Read files before asserting their content. Test before claiming something works.
|
|
38
|
-
- Tools available: task_done, bash, read_file, write_file, edit_file, glob, grep.
|
|
39
|
-
- When the current user message has been addressed, call task_done.
|
|
75
|
+
PROJECT CONTEXT (from POLYCODE.md in the user's project):
|
|
76
|
+
${projectContext.slice(0, 6000)}
|
|
40
77
|
|
|
41
|
-
|
|
78
|
+
When answering questions about the project, prefer the information in the PROJECT CONTEXT over reading files, unless the user explicitly asks you to read a file.`;
|
|
79
|
+
}
|
|
42
80
|
|
|
43
81
|
const TOOL_SCHEMAS = [
|
|
44
82
|
{
|
|
@@ -241,11 +279,12 @@ function recoverInlineToolCalls(content) {
|
|
|
241
279
|
}
|
|
242
280
|
|
|
243
281
|
export class AgenticLoop {
|
|
244
|
-
constructor({ apiKey, model, logger, rules } = {}) {
|
|
282
|
+
constructor({ apiKey, model, logger, rules, projectContext } = {}) {
|
|
245
283
|
this.apiKey = apiKey;
|
|
246
284
|
this.defaultModel = model || DEFAULT_MODEL;
|
|
247
285
|
this.logger = logger || console;
|
|
248
286
|
this.rules = rules || {};
|
|
287
|
+
this.systemPrompt = buildSystemPrompt(projectContext);
|
|
249
288
|
}
|
|
250
289
|
|
|
251
290
|
async runTurn({ canon, userMessage, cwd, onEvent, maxIterations }) {
|
|
@@ -318,7 +357,7 @@ export class AgenticLoop {
|
|
|
318
357
|
|
|
319
358
|
// Phase 3 + Phase 4: DISPATCH and ACT
|
|
320
359
|
const messages = [
|
|
321
|
-
{ role: 'system', content:
|
|
360
|
+
{ role: 'system', content: this.systemPrompt },
|
|
322
361
|
{ role: 'user', content: ctx.prompt },
|
|
323
362
|
];
|
|
324
363
|
|
package/lib/compiler.mjs
CHANGED
|
@@ -18,6 +18,74 @@ const COMPILER_MODEL = 'claude-haiku-4-5-20251001';
|
|
|
18
18
|
const COMPILER_MAX_TOKENS = 600;
|
|
19
19
|
const SUMMARY_ROW_LIMIT = 400;
|
|
20
20
|
|
|
21
|
+
// Conversational fast-path. Short user messages that look like greetings,
|
|
22
|
+
// acknowledgements, or self-questions skip the LLM compile step entirely
|
|
23
|
+
// and use a minimal pure-Node packet. Cuts per-turn latency from ~3s to
|
|
24
|
+
// sub-second for these cases and keeps the LLM from being told to continue
|
|
25
|
+
// any prior task when the user clearly is not asking about one.
|
|
26
|
+
const CONVERSATIONAL_MAX_LEN = 80;
|
|
27
|
+
const CONVERSATIONAL_PATTERNS = [
|
|
28
|
+
/^hi+\b/i,
|
|
29
|
+
/^hello\b/i,
|
|
30
|
+
/^hey\b/i,
|
|
31
|
+
/^yo\b/i,
|
|
32
|
+
/^howdy\b/i,
|
|
33
|
+
/^greetings\b/i,
|
|
34
|
+
/^thanks?\b/i,
|
|
35
|
+
/^thank you\b/i,
|
|
36
|
+
/^thx\b/i,
|
|
37
|
+
/^ty\b/i,
|
|
38
|
+
/^ok(ay)?\b/i,
|
|
39
|
+
/^cool\b/i,
|
|
40
|
+
/^nice\b/i,
|
|
41
|
+
/^great\b/i,
|
|
42
|
+
/^got it\b/i,
|
|
43
|
+
/^sure\b/i,
|
|
44
|
+
/^sounds good\b/i,
|
|
45
|
+
/^good\b/i,
|
|
46
|
+
/^how are you\b/i,
|
|
47
|
+
/^how('s| is) it going\b/i,
|
|
48
|
+
/^what's up\b/i,
|
|
49
|
+
/^sup\b/i,
|
|
50
|
+
/^who are you\b/i,
|
|
51
|
+
/^what are you\b/i,
|
|
52
|
+
/^what is polycode\b/i,
|
|
53
|
+
/^what can you do\b/i,
|
|
54
|
+
/^tell me about yourself\b/i,
|
|
55
|
+
/^bye\b/i,
|
|
56
|
+
/^goodbye\b/i,
|
|
57
|
+
/^see you\b/i,
|
|
58
|
+
/^later\b/i,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function isConversational(message) {
|
|
62
|
+
if (!message || typeof message !== 'string') return false;
|
|
63
|
+
const trimmed = message.trim();
|
|
64
|
+
if (trimmed.length === 0 || trimmed.length > CONVERSATIONAL_MAX_LEN) return false;
|
|
65
|
+
return CONVERSATIONAL_PATTERNS.some((re) => re.test(trimmed));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildConversationalPacket(userMessage, cwd) {
|
|
69
|
+
const lines = [
|
|
70
|
+
`[polycode conversational-fast-path]`,
|
|
71
|
+
`working_directory: ${cwd || process.cwd()}`,
|
|
72
|
+
'',
|
|
73
|
+
`CURRENT USER MESSAGE:`,
|
|
74
|
+
userMessage,
|
|
75
|
+
'',
|
|
76
|
+
`Respond with one short sentence and call task_done immediately. Do not use any tools.`,
|
|
77
|
+
];
|
|
78
|
+
const prompt = lines.join('\n');
|
|
79
|
+
return {
|
|
80
|
+
prompt,
|
|
81
|
+
estimatedTokens: Math.ceil(prompt.length / 4),
|
|
82
|
+
selectedRows: [],
|
|
83
|
+
compilerProvider: 'conversational-fast-path',
|
|
84
|
+
compilerUsage: null,
|
|
85
|
+
fallback: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
21
89
|
const COMPILER_SYSTEM_PROMPT = `You are a context selection helper. You receive a summary of a user's append-only session log (each row has an index, a type, and a short preview) and the user's current message. Your job is to return a JSON list of the row indices most relevant to the current message. You never write prose.
|
|
22
90
|
|
|
23
91
|
Selection rules:
|
|
@@ -83,6 +151,14 @@ async function selectRelevantRows(canon, userMessage) {
|
|
|
83
151
|
}
|
|
84
152
|
|
|
85
153
|
export async function compilePacket(canon, userMessage, cwd) {
|
|
154
|
+
// Fast-path: trivial conversational messages skip the LLM compile step and
|
|
155
|
+
// get a minimal packet that tells the generator to respond briefly and stop.
|
|
156
|
+
// This structurally prevents prior-session contamination from poisoning
|
|
157
|
+
// short greetings and acknowledgements.
|
|
158
|
+
if (isConversational(userMessage)) {
|
|
159
|
+
return buildConversationalPacket(userMessage, cwd);
|
|
160
|
+
}
|
|
161
|
+
|
|
86
162
|
const selection = await selectRelevantRows(canon, userMessage);
|
|
87
163
|
const selectedIndices = new Set(selection.selected || []);
|
|
88
164
|
|
package/lib/repl-ui.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// lib/repl-ui.mjs
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// REPL output renderer. Default mode is compact: show a single collapsed
|
|
3
|
+
// line per tool call, show the agent's text response prominently, hide the
|
|
4
|
+
// internal phase trace. Verbose mode (--verbose) shows every phase, every
|
|
5
|
+
// tool call argument, every tool result preview, every record event.
|
|
6
|
+
// Zero external dependencies; ANSI colors only.
|
|
6
7
|
|
|
7
8
|
export const C = {
|
|
8
9
|
reset: '\x1b[0m',
|
|
@@ -50,10 +51,48 @@ function phasePrefix(name) {
|
|
|
50
51
|
return map[name] || `${C.gray}. ${name}${C.reset}`;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
// Compact-mode tool call renderer. Produces a single line per tool call
|
|
55
|
+
// that names the tool and its most-useful argument, no JSON dump.
|
|
56
|
+
function compactToolCallLine(name, args) {
|
|
57
|
+
const a = args || {};
|
|
58
|
+
let arg = '';
|
|
59
|
+
if (name === 'read_file' || name === 'write_file' || name === 'edit_file') {
|
|
60
|
+
arg = a.path || '';
|
|
61
|
+
} else if (name === 'bash') {
|
|
62
|
+
arg = String(a.command || '').slice(0, 60);
|
|
63
|
+
} else if (name === 'glob') {
|
|
64
|
+
arg = a.pattern || '';
|
|
65
|
+
} else if (name === 'grep') {
|
|
66
|
+
arg = a.pattern || '';
|
|
67
|
+
if (a.glob) arg += ` in ${a.glob}`;
|
|
68
|
+
} else if (name === 'task_done') {
|
|
69
|
+
return null; // task_done is implicit in the final response, not shown as an action
|
|
70
|
+
}
|
|
71
|
+
return `${C.dim}${C.gray}·${C.reset} ${C.bold}${name}${C.reset}${C.dim}${arg ? ` ${arg}` : ''}${C.reset}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Compact-mode tool result renderer. A single short line per result.
|
|
75
|
+
function compactToolResultLine(name, result) {
|
|
76
|
+
if (name === 'task_done') return null;
|
|
77
|
+
const r = String(result || '');
|
|
78
|
+
if (r.startsWith('error:')) {
|
|
79
|
+
return `${C.dim}${C.red} ${r.slice(0, 140)}${C.reset}`;
|
|
80
|
+
}
|
|
81
|
+
if (r.startsWith('ok:')) {
|
|
82
|
+
return `${C.dim}${C.green} ${r.slice(0, 140)}${C.reset}`;
|
|
83
|
+
}
|
|
84
|
+
// Default: one-line truncated preview
|
|
85
|
+
const preview = r.slice(0, 140).replace(/\n/g, ' ');
|
|
86
|
+
return `${C.dim} ${preview}${C.reset}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createRenderer(stdout, opts = {}) {
|
|
90
|
+
const verbose = Boolean(opts.verbose);
|
|
91
|
+
|
|
54
92
|
function line(s) { stdout.write(s + '\n'); }
|
|
55
93
|
|
|
56
|
-
|
|
94
|
+
// Verbose event handler: shows every phase, tool call, result, record.
|
|
95
|
+
function onEventVerbose(ev) {
|
|
57
96
|
if (ev.phase === 'intent') {
|
|
58
97
|
line(`${phasePrefix('intent')} ${C.dim}active intent resolved${C.reset}`);
|
|
59
98
|
} else if (ev.phase === 'ground_complete') {
|
|
@@ -87,5 +126,30 @@ export function createRenderer(stdout) {
|
|
|
87
126
|
}
|
|
88
127
|
}
|
|
89
128
|
|
|
90
|
-
|
|
129
|
+
// Compact event handler: shows tool calls as single-line actions, shows
|
|
130
|
+
// the agent's text response, hides all internal tracing.
|
|
131
|
+
function onEventCompact(ev) {
|
|
132
|
+
if (ev.phase === 'act' && ev.kind === 'message') {
|
|
133
|
+
const content = String(ev.content || '').trim();
|
|
134
|
+
if (content) line(`${C.cyan}${content}${C.reset}`);
|
|
135
|
+
} else if (ev.phase === 'act' && ev.kind === 'tool_call') {
|
|
136
|
+
const out = compactToolCallLine(ev.name, ev.args);
|
|
137
|
+
if (out) line(out);
|
|
138
|
+
} else if (ev.phase === 'act' && ev.kind === 'tool_result') {
|
|
139
|
+
const out = compactToolResultLine(ev.name, ev.result);
|
|
140
|
+
if (out) line(out);
|
|
141
|
+
} else if (ev.phase === 'scrub_blocked') {
|
|
142
|
+
line(`${C.red}refusing to send tool output to the model: contains a recognized secret pattern${C.reset}`);
|
|
143
|
+
} else if (ev.phase === 'error') {
|
|
144
|
+
line(`${C.red}error: ${ev.message}${C.reset}`);
|
|
145
|
+
}
|
|
146
|
+
// All other phases hidden in compact mode.
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
onEvent: verbose ? onEventVerbose : onEventCompact,
|
|
151
|
+
verbose,
|
|
152
|
+
C,
|
|
153
|
+
verdictPill,
|
|
154
|
+
};
|
|
91
155
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polylogicai/polycode",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "An agentic coding CLI. Runs on your machine with your keys. Every turn is appended to a SHA-256 chained session log, so your history is auditable, replayable, and portable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/polycode.mjs",
|