@oxecli/oxe 1.0.27 → 1.0.28

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/dist/cli.js CHANGED
@@ -58,9 +58,8 @@ export class CLI {
58
58
  titleAlign: "left",
59
59
  colGap: 2,
60
60
  });
61
- // Write the panel starting on a fresh line. No extra leading/trailing blank
62
- // here askBottomPrompt's own leading newline supplies the single gap.
63
- process.stdout.write(panel + "\n");
61
+ // Keep one blank row between the previous response/footer and the panel.
62
+ process.stdout.write("\n" + panel + "\n");
64
63
  if (process.stdin.isTTY) {
65
64
  // Wait for a close key, then erase the panel (mirrors Python's Live).
66
65
  enableRawStdin();
@@ -336,6 +335,7 @@ export class CLI {
336
335
  for (;;) {
337
336
  engine.inQuery = false;
338
337
  let rawPrompt;
338
+ let queryStarted = false;
339
339
  try {
340
340
  const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory);
341
341
  if (!input.trim())
@@ -390,7 +390,9 @@ export class CLI {
390
390
  continue;
391
391
  }
392
392
  process.stdout.write(`\n\x1b[1m❯\x1b[0m ${userDisplayText(input, spans)}\n\n`);
393
+ queryStarted = true;
393
394
  await engine.executeQuery(input, this.inputItems, this.story, spans);
395
+ queryStarted = false;
394
396
  this.saveSession(engine);
395
397
  }
396
398
  catch (err) {
@@ -401,7 +403,7 @@ export class CLI {
401
403
  break;
402
404
  }
403
405
  if (err?.message === "interrupt") {
404
- const wasActive = engine.inQuery;
406
+ const wasActive = queryStarted || engine.inQuery;
405
407
  engine.inQuery = false;
406
408
  // Drop the pending user message/story entry that was never run.
407
409
  if (this.inputItems.length && this.inputItems[this.inputItems.length - 1]["role"] === "user") {
@@ -422,8 +424,8 @@ export class CLI {
422
424
  // (mirrors Python's console.print()).
423
425
  process.stdout.write("\n");
424
426
  process.stdout.write(wasActive
425
- ? "\x1b[2mInterrupted. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n"
426
- : "\x1b[2mNo active response. Press Ctrl+C again to exit.\x1b[0m\n");
427
+ ? "\x1b[2mInterrupted agent. Press Ctrl+C again to close the app, or type a new prompt.\x1b[0m\n"
428
+ : "\x1b[2mNo active response. Press Ctrl+C again to close the app, or type a new prompt.\x1b[0m\n");
427
429
  continue;
428
430
  }
429
431
  throw err;
@@ -451,10 +453,19 @@ export async function main() {
451
453
  }
452
454
  app.config = config;
453
455
  const engine = new InferenceEngine(config);
456
+ const onInterrupt = () => {
457
+ if (engine.inQuery) {
458
+ engine.interrupt();
459
+ return;
460
+ }
461
+ process.exit(130);
462
+ };
463
+ process.on("SIGINT", onInterrupt);
454
464
  try {
455
465
  await app.replLoop(engine);
456
466
  }
457
467
  finally {
468
+ process.removeListener("SIGINT", onInterrupt);
458
469
  await engine.cleanupStoredResponses();
459
470
  const client = engine.client;
460
471
  if (typeof client?.close === "function")
package/dist/engine.js CHANGED
@@ -47,6 +47,8 @@ export class InferenceEngine {
47
47
  workActive = false;
48
48
  workRows = 2;
49
49
  inQuery = false;
50
+ activeAbort = null;
51
+ interrupted = false;
50
52
  temperature;
51
53
  storedResponseIds = [];
52
54
  constructor(config) {
@@ -62,6 +64,10 @@ export class InferenceEngine {
62
64
  });
63
65
  this.temperature = config["temperature"] ?? null;
64
66
  }
67
+ interrupt() {
68
+ this.interrupted = true;
69
+ this.activeAbort?.abort();
70
+ }
65
71
  async cleanupStoredResponses() {
66
72
  const ids = this.storedResponseIds;
67
73
  this.storedResponseIds = [];
@@ -233,8 +239,12 @@ export class InferenceEngine {
233
239
  kwargs["temperature"] = this.temperature;
234
240
  if (previousResponseId)
235
241
  kwargs["previous_response_id"] = previousResponseId;
236
- const stream = await this.client.responses.create(kwargs);
242
+ const stream = await this.client.responses.create(kwargs, {
243
+ signal: this.activeAbort?.signal,
244
+ });
237
245
  for await (const event of stream) {
246
+ if (this.interrupted)
247
+ throw new Error("interrupt");
238
248
  etype = event.type;
239
249
  if (etype === "response.reasoning_text.delta" ||
240
250
  etype === "response.reasoning.summary.delta") {
@@ -413,6 +423,8 @@ export class InferenceEngine {
413
423
  }
414
424
  async executeQuery(userPrompt, inputItems, story, pasteSpans) {
415
425
  this.inQuery = true;
426
+ this.interrupted = false;
427
+ this.activeAbort = new AbortController();
416
428
  hideCursor();
417
429
  inputItems.push({
418
430
  role: "user",
@@ -538,6 +550,8 @@ export class InferenceEngine {
538
550
  if (c.name === "edit_file" || c.name === "write_file")
539
551
  process.stdout.write("\n");
540
552
  const rawResult = await this.runTool(c.name, c.arguments);
553
+ if (this.interrupted)
554
+ throw new Error("interrupt");
541
555
  const failed = toolOutputFailed(c.name, rawResult);
542
556
  const action = formatToolAction(c.name, c.arguments, failed ? "failed" : "ok");
543
557
  story.push({ type: "tool", started, text: action, status: failed ? "failed" : "ok" });
@@ -568,6 +582,8 @@ export class InferenceEngine {
568
582
  stripOrphanCalls(inputItems);
569
583
  }
570
584
  catch (err) {
585
+ if (this.interrupted)
586
+ throw new Error("interrupt");
571
587
  if (err?.message === "interrupt" || err?.message === "eof") {
572
588
  throw err;
573
589
  }
@@ -575,6 +591,7 @@ export class InferenceEngine {
575
591
  }
576
592
  finally {
577
593
  this.inQuery = false;
594
+ this.activeAbort = null;
578
595
  if (stats["tokens"] > 0 && this.keyData) {
579
596
  const userId = this.keyData["user_id"];
580
597
  if (userId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },