@phren/agent 0.1.2 → 0.1.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.
@@ -194,73 +194,129 @@ export class CodexProvider {
194
194
  body.tools = toResponsesTools(tools);
195
195
  body.tool_choice = "auto";
196
196
  }
197
- const res = await fetch(CODEX_API, {
198
- method: "POST",
197
+ // Use WebSocket for true token-by-token streaming (matches Codex CLI behavior).
198
+ // The HTTP SSE endpoint batches the entire response before flushing.
199
+ yield* this.chatStreamWs(accessToken, body);
200
+ }
201
+ /** WebSocket streaming — sends request, yields deltas as they arrive. */
202
+ async *chatStreamWs(accessToken, body) {
203
+ const wsUrl = CODEX_API.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
204
+ // Queue for events received from the WebSocket before the consumer pulls them
205
+ const queue = [];
206
+ let resolve = null;
207
+ let done = false;
208
+ const push = (item) => {
209
+ queue.push(item);
210
+ if (resolve) {
211
+ resolve();
212
+ resolve = null;
213
+ }
214
+ };
215
+ // Node.js (undici) WebSocket accepts headers in the second argument object,
216
+ // but the DOM typings only allow string | string[]. Cast to bypass.
217
+ const ws = new WebSocket(wsUrl, {
199
218
  headers: {
200
- "Content-Type": "application/json",
201
219
  Authorization: `Bearer ${accessToken}`,
202
220
  },
203
- body: JSON.stringify(body),
204
221
  });
205
- if (!res.ok) {
206
- const text = await res.text();
207
- throw new Error(`Codex API error ${res.status}: ${text}`);
208
- }
209
- // Parse SSE stream
210
- if (!res.body)
211
- throw new Error("Provider returned empty response body");
212
- const reader = res.body.getReader();
213
- const decoder = new TextDecoder();
214
- let buffer = "";
215
222
  let activeToolCallId = "";
216
- while (true) {
217
- const { done, value } = await reader.read();
218
- if (done)
219
- break;
220
- buffer += decoder.decode(value, { stream: true });
221
- const lines = buffer.split("\n");
222
- buffer = lines.pop();
223
- for (const line of lines) {
224
- if (!line.startsWith("data: "))
225
- continue;
226
- const data = line.slice(6).trim();
227
- if (data === "[DONE]")
228
- return;
229
- let event;
223
+ ws.addEventListener("open", () => {
224
+ // Wrap the request body in a response.create envelope (Codex WS protocol)
225
+ const wsRequest = { type: "response.create", ...body };
226
+ ws.send(JSON.stringify(wsRequest));
227
+ });
228
+ ws.addEventListener("message", (evt) => {
229
+ const data = typeof evt.data === "string" ? evt.data : String(evt.data);
230
+ let event;
231
+ try {
232
+ event = JSON.parse(data);
233
+ }
234
+ catch {
235
+ return;
236
+ }
237
+ const type = event.type;
238
+ // Handle server-side errors
239
+ if (type === "error") {
240
+ const err = event.error;
241
+ const msg = err?.message ?? "Codex WebSocket error";
242
+ const status = event.status;
243
+ push(new Error(`Codex WS error${status ? ` ${status}` : ""}: ${msg}`));
244
+ done = true;
230
245
  try {
231
- event = JSON.parse(data);
232
- }
233
- catch {
234
- continue;
246
+ ws.close();
235
247
  }
236
- const type = event.type;
237
- if (type === "response.output_text.delta") {
238
- yield { type: "text_delta", text: event.delta };
239
- }
240
- else if (type === "response.output_item.added") {
241
- if (event.item?.type === "function_call") {
242
- const item = event.item;
243
- activeToolCallId = item.call_id;
244
- yield { type: "tool_use_start", id: activeToolCallId, name: item.name };
245
- }
248
+ catch { /* ignore */ }
249
+ return;
250
+ }
251
+ if (type === "response.output_text.delta") {
252
+ const delta = event.delta;
253
+ if (delta)
254
+ push({ type: "text_delta", text: delta });
255
+ }
256
+ else if (type === "response.output_item.added") {
257
+ if (event.item?.type === "function_call") {
258
+ const item = event.item;
259
+ activeToolCallId = item.call_id;
260
+ push({ type: "tool_use_start", id: activeToolCallId, name: item.name });
246
261
  }
247
- else if (type === "response.function_call_arguments.delta") {
248
- yield { type: "tool_use_delta", id: activeToolCallId, json: event.delta };
262
+ }
263
+ else if (type === "response.function_call_arguments.delta") {
264
+ push({ type: "tool_use_delta", id: activeToolCallId, json: event.delta });
265
+ }
266
+ else if (type === "response.function_call_arguments.done") {
267
+ push({ type: "tool_use_end", id: activeToolCallId });
268
+ }
269
+ else if (type === "response.completed") {
270
+ const response = event.response;
271
+ const usage = response?.usage;
272
+ const output = response?.output;
273
+ const hasToolCalls = output?.some((o) => o.type === "function_call");
274
+ push({
275
+ type: "done",
276
+ stop_reason: hasToolCalls ? "tool_use" : "end_turn",
277
+ usage: usage ? { input_tokens: usage.input_tokens ?? 0, output_tokens: usage.output_tokens ?? 0 } : undefined,
278
+ });
279
+ done = true;
280
+ try {
281
+ ws.close();
249
282
  }
250
- else if (type === "response.function_call_arguments.done") {
251
- yield { type: "tool_use_end", id: activeToolCallId };
283
+ catch { /* ignore */ }
284
+ }
285
+ });
286
+ ws.addEventListener("error", () => {
287
+ if (!done) {
288
+ push(new Error("Codex WebSocket connection error"));
289
+ done = true;
290
+ }
291
+ });
292
+ ws.addEventListener("close", () => {
293
+ if (!done) {
294
+ push(new Error("Codex WebSocket closed before response.completed"));
295
+ done = true;
296
+ }
297
+ });
298
+ // Async iteration: drain the queue, wait for new events
299
+ try {
300
+ while (true) {
301
+ while (queue.length > 0) {
302
+ const item = queue.shift();
303
+ if (item instanceof Error)
304
+ throw item;
305
+ yield item;
306
+ if (item.type === "done")
307
+ return;
252
308
  }
253
- else if (type === "response.completed") {
254
- const response = event.response;
255
- const usage = response?.usage;
256
- const output = response?.output;
257
- const hasToolCalls = output?.some((o) => o.type === "function_call");
258
- yield {
259
- type: "done",
260
- stop_reason: hasToolCalls ? "tool_use" : "end_turn",
261
- usage: usage ? { input_tokens: usage.input_tokens ?? 0, output_tokens: usage.output_tokens ?? 0 } : undefined,
262
- };
309
+ if (done)
310
+ return;
311
+ await new Promise((r) => { resolve = r; });
312
+ }
313
+ }
314
+ finally {
315
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
316
+ try {
317
+ ws.close();
263
318
  }
319
+ catch { /* ignore */ }
264
320
  }
265
321
  }
266
322
  }
package/dist/repl.js CHANGED
@@ -82,7 +82,7 @@ export async function startRepl(config) {
82
82
  rl.prompt();
83
83
  continue;
84
84
  }
85
- if (handleCommand(trimmed, { session, contextLimit, undoStack: [] })) {
85
+ if (handleCommand(trimmed, { session, contextLimit, undoStack: [], phrenCtx: config.phrenCtx })) {
86
86
  rl.prompt();
87
87
  continue;
88
88
  }
@@ -118,7 +118,7 @@ export async function startRepl(config) {
118
118
  process.stderr.write(`${YELLOW}Input mode: ${inputMode}${RESET}\n`);
119
119
  }
120
120
  else {
121
- handleCommand(queued, { session, contextLimit, undoStack: [] });
121
+ handleCommand(queued, { session, contextLimit, undoStack: [], phrenCtx: config.phrenCtx });
122
122
  }
123
123
  break;
124
124
  }
@@ -1,37 +1,35 @@
1
1
  export function buildSystemPrompt(phrenContext, priorSummary, providerInfo) {
2
2
  const modelNote = providerInfo ? ` You are running on ${providerInfo.name}${providerInfo.model ? ` (model: ${providerInfo.model})` : ""}.` : "";
3
3
  const parts = [
4
- `You are phren-agent, a coding assistant with persistent memory powered by phren.${modelNote} You retain knowledge across sessions — past decisions, discovered patterns, and project context are all searchable. Use this memory to avoid repeating mistakes and to build on prior work.`,
4
+ `You are phren-agent, an autonomous coding agent with persistent memory.${modelNote}`,
5
5
  "",
6
- "## Workflow",
7
- "1. **Orient** Before starting, search phren for relevant findings (`phren_search`) and check active tasks (`phren_get_tasks`). Past sessions may have context that saves time.",
8
- "2. **Read** — Read the relevant code before modifying it. Use `glob` to find files, `grep` to locate symbols, `read_file` to understand context.",
9
- "3. **Change** Make targeted edits. Use `edit_file` for surgical changes; reserve `write_file` for new files. Don't refactor code you weren't asked to touch.",
10
- "4. **Verify** — Run tests and linters via `shell` after edits. Check `git_diff` to review your changes.",
11
- "5. **Remember** — Save non-obvious discoveries with `phren_add_finding`: tricky bugs, architecture decisions, gotchas, workarounds. Skip obvious things — only save what would help a future session.",
12
- "6. **Report** — Explain what you did concisely. Mention files changed and why.",
6
+ "## Core Behavior",
7
+ "ACT IMMEDIATELY. When the user asks you to do something, DO IT. Don't describe what you're going to do just do it. Use your tools without asking permission. Read files, search code, make edits, run commands. Only ask clarifying questions when the request is genuinely ambiguous.",
8
+ "",
9
+ "You have persistent memory via phren. Past decisions, discovered patterns, and project context are searchable across sessions. Use this to avoid repeating mistakes.",
13
10
  "",
14
- "## Memory",
15
- "- `phren_search` finds past findings, reference docs, and project context. Search before asking the user for context they may have already provided.",
16
- "- `phren_add_finding` saves insights for future sessions. Good findings: non-obvious patterns, decisions with rationale, error resolutions, architecture constraints. Bad findings: narration of what you did, obvious facts, secrets.",
17
- "- `phren_get_tasks` shows tracked work items. Complete tasks with `phren_complete_task` when done.",
11
+ "## Workflow",
12
+ "1. **Search memory first** `phren_search` for relevant past findings before starting work.",
13
+ "2. **Read before writing** `glob` to find files, `grep` to locate symbols, `read_file` to understand code.",
14
+ "3. **Make changes** `edit_file` for surgical edits, `write_file` for new files only.",
15
+ "4. **Verify** — `shell` to run tests/linters, `git_diff` to review changes.",
16
+ "5. **Save learnings** — `phren_add_finding` for non-obvious discoveries (bugs, architecture decisions, gotchas). Skip obvious stuff.",
17
+ "6. **Report concisely** — what changed and why. No fluff.",
18
18
  "",
19
- "## Self-configuration",
20
- "You ARE phren-agent. You can configure phren itself via shell commands:",
21
- "- `phren init` set up phren (MCP server, hooks, profiles)",
22
- "- `phren add <path>` register a project directory",
23
- "- `phren config proactivity <level>` — set proactivity (high/medium/low)",
24
- "- `phren config policy set <key> <value>` — configure retention, TTL, decay",
25
- "- `phren hooks enable <tool>` — enable hooks for claude/copilot/cursor/codex",
26
- "- `phren doctor --fix` — diagnose and self-heal",
27
- "- `phren status` — check health",
28
- "If the user asks you to configure phren, set up a project, or fix their install, use the shell tool to run these commands.",
19
+ "## Tools You Have",
20
+ "- File I/O: `read_file`, `write_file`, `edit_file`",
21
+ "- Search: `glob`, `grep`, `web_search`, `web_fetch`",
22
+ "- System: `shell` (run commands, cd, build, test)",
23
+ "- Git: `git_status`, `git_diff`, `git_commit`",
24
+ "- Memory: `phren_search`, `phren_add_finding`, `phren_get_tasks`, `phren_complete_task`, `phren_add_task`",
29
25
  "",
30
- "## Rules",
26
+ "## Important",
27
+ "- Be direct and concise. Lead with the answer, not the reasoning.",
28
+ "- Call multiple tools in parallel when they're independent.",
29
+ "- NEVER ask 'should I read the file?' or 'would you like me to...' — just call the tool. If permission is needed, the system will prompt the user automatically. You don't handle permissions.",
30
+ "- Don't describe your plan unless asked. Execute immediately.",
31
31
  "- Never write secrets, API keys, or PII to files or findings.",
32
- "- Prefer `edit_file` over `write_file` for existing files.",
33
- "- Keep shell commands safe. No `rm -rf`, no `sudo`, no destructive operations.",
34
- "- If unsure, say so. Don't guess at behavior you can verify by reading code or running tests.",
32
+ "- You ARE phren-agent. You can run `phren` CLI commands via shell to configure yourself.",
35
33
  ];
36
34
  if (priorSummary) {
37
35
  parts.push("", `## Last session\n${priorSummary}`);
@@ -5,7 +5,7 @@ const MAX_TIMEOUT_MS = 120_000;
5
5
  const MAX_OUTPUT_BYTES = 100_000;
6
6
  export const shellTool = {
7
7
  name: "shell",
8
- description: "Run a shell command via bash -c and return stdout + stderr. Use for: running tests, linters, build commands, git operations, and exploring the environment. Prefer specific tools (read_file, glob, grep) over shell equivalents when available.",
8
+ description: "Run a shell command and return stdout + stderr. Uses bash on Unix, cmd.exe on Windows. Use for: running tests, linters, build commands, git operations, and exploring the environment. Prefer specific tools (read_file, glob, grep) over shell equivalents when available.",
9
9
  input_schema: {
10
10
  type: "object",
11
11
  properties: {
@@ -24,8 +24,11 @@ export const shellTool = {
24
24
  if (!safety.safe && safety.severity === "block") {
25
25
  return { output: `Blocked: ${safety.reason}`, is_error: true };
26
26
  }
27
+ const isWindows = process.platform === "win32";
28
+ const shell = isWindows ? "cmd" : "bash";
29
+ const shellArgs = isWindows ? ["/c", command] : ["-c", command];
27
30
  try {
28
- const output = execFileSync("bash", ["-c", command], {
31
+ const output = execFileSync(shell, shellArgs, {
29
32
  cwd,
30
33
  encoding: "utf-8",
31
34
  timeout,