@pi-ohm/subagents 0.6.4-dev.22132543465.1.e4e3071 → 0.6.4-dev.22169815567.1.cdde4e8

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.
@@ -3,11 +3,18 @@ import test from "node:test";
3
3
  import type { OhmRuntimeConfig, OhmSubagentBackend } from "@pi-ohm/config";
4
4
  import { Result } from "better-result";
5
5
  import type { OhmSubagentDefinition } from "../catalog";
6
+ import { SubagentRuntimeError } from "../errors";
6
7
  import {
8
+ applyPiSdkSessionEvent,
7
9
  createDefaultTaskExecutionBackend,
10
+ createPiSdkStreamCaptureState,
11
+ finalizePiSdkStreamCapture,
8
12
  PiCliTaskExecutionBackend,
13
+ PiSdkTaskExecutionBackend,
9
14
  ScaffoldTaskExecutionBackend,
10
15
  type PiCliRunner,
16
+ type PiSdkRunner,
17
+ type TaskExecutionBackend,
11
18
  } from "./backend";
12
19
 
13
20
  function defineTest(name: string, run: () => void | Promise<void>): void {
@@ -197,7 +204,11 @@ defineTest(
197
204
  assert.equal(prompts.length, 1);
198
205
  assert.match(prompts[0] ?? "", /You are the Finder subagent in Pi OHM/);
199
206
  assert.equal(result.value.output, "finder online");
200
- assert.match(result.value.summary, /Finder: finder online/);
207
+ assert.match(result.value.summary, /Finder: Auth flow scan/);
208
+ assert.equal(result.value.runtime, "pi-cli");
209
+ assert.equal(result.value.route, "interactive-shell");
210
+ assert.equal(result.value.provider, "unavailable");
211
+ assert.equal(result.value.model, "unavailable");
201
212
  },
202
213
  );
203
214
 
@@ -238,6 +249,442 @@ defineTest(
238
249
  },
239
250
  );
240
251
 
252
+ defineTest(
253
+ "PiCliTaskExecutionBackend strips backend/provider/model metadata lines from nested output",
254
+ async () => {
255
+ const runner: PiCliRunner = async () => ({
256
+ exitCode: 0,
257
+ stdout: [
258
+ "backend: pi-coding-agent",
259
+ "provider: openai-codex",
260
+ "model: gpt-5.3-codex",
261
+ "",
262
+ "actual line one",
263
+ "actual line two",
264
+ ].join("\n"),
265
+ stderr: "",
266
+ timedOut: false,
267
+ aborted: false,
268
+ });
269
+
270
+ const backend = new PiCliTaskExecutionBackend(runner, 1_000);
271
+ const result = await backend.executeStart({
272
+ taskId: "task_9",
273
+ subagent: subagentFixture,
274
+ description: "Auth flow scan",
275
+ prompt: "Find auth validation path and refresh flow",
276
+ cwd: "/tmp/project",
277
+ config: makeConfig("interactive-shell"),
278
+ signal: undefined,
279
+ });
280
+
281
+ assert.equal(Result.isOk(result), true);
282
+ if (Result.isError(result)) {
283
+ assert.fail("Expected sanitized output success");
284
+ }
285
+
286
+ assert.doesNotMatch(result.value.output, /^backend:/m);
287
+ assert.doesNotMatch(result.value.output, /^provider:/m);
288
+ assert.doesNotMatch(result.value.output, /^model:/m);
289
+ assert.match(result.value.output, /actual line one/);
290
+ assert.match(result.value.output, /actual line two/);
291
+ assert.equal(result.value.provider, "openai-codex");
292
+ assert.equal(result.value.model, "gpt-5.3-codex");
293
+ assert.equal(result.value.runtime, "pi-coding-agent");
294
+ assert.equal(result.value.route, "interactive-shell");
295
+ },
296
+ );
297
+
298
+ defineTest("PiSdkTaskExecutionBackend executes sdk runner for interactive-sdk", async () => {
299
+ const prompts: string[] = [];
300
+
301
+ const runner: PiSdkRunner = async (input) => {
302
+ prompts.push(input.prompt);
303
+ return {
304
+ output: "sdk online",
305
+ events: [],
306
+ provider: "sdk-provider",
307
+ model: "sdk-model",
308
+ runtime: "pi-sdk",
309
+ timedOut: false,
310
+ aborted: false,
311
+ };
312
+ };
313
+
314
+ const backend = new PiSdkTaskExecutionBackend(runner, 1_000);
315
+ const result = await backend.executeStart({
316
+ taskId: "task_sdk_1",
317
+ subagent: subagentFixture,
318
+ description: "Auth flow scan",
319
+ prompt: "Trace auth validation path",
320
+ cwd: "/tmp/project",
321
+ config: makeConfig("interactive-sdk"),
322
+ signal: undefined,
323
+ });
324
+
325
+ assert.equal(Result.isOk(result), true);
326
+ if (Result.isError(result)) {
327
+ assert.fail("Expected interactive-sdk backend execution to succeed");
328
+ }
329
+
330
+ assert.equal(prompts.length, 1);
331
+ assert.match(prompts[0] ?? "", /You are the Finder subagent in Pi OHM/);
332
+ assert.equal(result.value.output, "sdk online");
333
+ assert.equal(result.value.provider, "sdk-provider");
334
+ assert.equal(result.value.model, "sdk-model");
335
+ assert.equal(result.value.runtime, "pi-sdk");
336
+ assert.equal(result.value.route, "interactive-sdk");
337
+ });
338
+
339
+ defineTest("Pi SDK stream capture records tool lifecycle and assistant deltas", () => {
340
+ const capture = createPiSdkStreamCaptureState();
341
+
342
+ applyPiSdkSessionEvent(capture, {
343
+ type: "tool_execution_start",
344
+ toolCallId: "tool_1",
345
+ toolName: "read",
346
+ args: { path: "src/index.ts" },
347
+ });
348
+ applyPiSdkSessionEvent(capture, {
349
+ type: "tool_execution_update",
350
+ toolCallId: "tool_1",
351
+ toolName: "read",
352
+ partialResult: { progress: "50%" },
353
+ });
354
+ applyPiSdkSessionEvent(capture, {
355
+ type: "tool_execution_end",
356
+ toolCallId: "tool_1",
357
+ toolName: "read",
358
+ result: { ok: true },
359
+ isError: false,
360
+ });
361
+ applyPiSdkSessionEvent(capture, {
362
+ type: "message_update",
363
+ assistantMessageEvent: { type: "text_delta", delta: "Found auth flow." },
364
+ });
365
+ applyPiSdkSessionEvent(capture, {
366
+ type: "agent_end",
367
+ });
368
+
369
+ const finalized = finalizePiSdkStreamCapture(capture);
370
+ assert.equal(finalized.sawAgentEnd, true);
371
+ assert.equal(finalized.capturedEventCount, 5);
372
+ assert.match(finalized.output, /tool_call: read start/);
373
+ assert.match(finalized.output, /tool_call: read update/);
374
+ assert.match(finalized.output, /tool_call: read end success/);
375
+ assert.match(finalized.output, /Found auth flow\./);
376
+ });
377
+
378
+ defineTest("Pi SDK stream capture ignores unsupported events", () => {
379
+ const capture = createPiSdkStreamCaptureState();
380
+
381
+ applyPiSdkSessionEvent(capture, { type: "turn_start", turnIndex: 1 });
382
+ applyPiSdkSessionEvent(capture, {
383
+ type: "message_update",
384
+ assistantMessageEvent: { type: "thinking_delta", delta: "..." },
385
+ });
386
+ applyPiSdkSessionEvent(capture, {
387
+ type: "tool_execution_start",
388
+ toolCallId: "tool_1",
389
+ toolName: "bash",
390
+ args: undefined,
391
+ });
392
+
393
+ const finalized = finalizePiSdkStreamCapture(capture);
394
+ assert.equal(finalized.capturedEventCount, 1);
395
+ assert.equal(finalized.sawAgentEnd, false);
396
+ assert.match(finalized.output, /tool_call: bash start/);
397
+ });
398
+
399
+ defineTest("PiSdkTaskExecutionBackend maps timeout/abort/execution failures", async () => {
400
+ const timeoutBackend = new PiSdkTaskExecutionBackend(async () => ({
401
+ output: "",
402
+ events: [],
403
+ timedOut: true,
404
+ aborted: false,
405
+ }));
406
+
407
+ const timedOut = await timeoutBackend.executeStart({
408
+ taskId: "task_sdk_timeout",
409
+ subagent: subagentFixture,
410
+ description: "timeout",
411
+ prompt: "timeout",
412
+ cwd: "/tmp/project",
413
+ config: makeConfig("interactive-sdk"),
414
+ signal: undefined,
415
+ });
416
+
417
+ assert.equal(Result.isError(timedOut), true);
418
+ if (Result.isOk(timedOut)) {
419
+ assert.fail("Expected timeout error");
420
+ }
421
+ assert.equal(timedOut.error.code, "task_backend_timeout");
422
+
423
+ const abortedBackend = new PiSdkTaskExecutionBackend(async () => ({
424
+ output: "",
425
+ events: [],
426
+ timedOut: false,
427
+ aborted: true,
428
+ }));
429
+
430
+ const aborted = await abortedBackend.executeStart({
431
+ taskId: "task_sdk_abort",
432
+ subagent: subagentFixture,
433
+ description: "abort",
434
+ prompt: "abort",
435
+ cwd: "/tmp/project",
436
+ config: makeConfig("interactive-sdk"),
437
+ signal: undefined,
438
+ });
439
+
440
+ assert.equal(Result.isError(aborted), true);
441
+ if (Result.isOk(aborted)) {
442
+ assert.fail("Expected aborted error");
443
+ }
444
+ assert.equal(aborted.error.code, "task_aborted");
445
+
446
+ const failedBackend = new PiSdkTaskExecutionBackend(async () => ({
447
+ output: "",
448
+ events: [],
449
+ timedOut: false,
450
+ aborted: false,
451
+ error: "sdk failed",
452
+ }));
453
+
454
+ const failed = await failedBackend.executeSend({
455
+ taskId: "task_sdk_fail",
456
+ subagent: subagentFixture,
457
+ description: "failure",
458
+ initialPrompt: "initial",
459
+ followUpPrompts: [],
460
+ prompt: "follow-up",
461
+ cwd: "/tmp/project",
462
+ config: makeConfig("interactive-sdk"),
463
+ signal: undefined,
464
+ });
465
+
466
+ assert.equal(Result.isError(failed), true);
467
+ if (Result.isOk(failed)) {
468
+ assert.fail("Expected execution failure");
469
+ }
470
+ assert.equal(failed.error.code, "task_backend_execution_failed");
471
+ });
472
+
473
+ defineTest("PiCliTaskExecutionBackend delegates to sdk backend when configured", async () => {
474
+ let cliCalled = false;
475
+
476
+ const cliRunner: PiCliRunner = async () => {
477
+ cliCalled = true;
478
+ return {
479
+ exitCode: 0,
480
+ stdout: "cli output",
481
+ stderr: "",
482
+ timedOut: false,
483
+ aborted: false,
484
+ };
485
+ };
486
+
487
+ const sdkBackend: TaskExecutionBackend = {
488
+ id: "interactive-sdk",
489
+ async executeStart() {
490
+ return Result.ok({
491
+ summary: "Finder: via sdk",
492
+ output: "sdk output",
493
+ provider: "sdk-provider",
494
+ model: "sdk-model",
495
+ runtime: "pi-sdk",
496
+ route: "interactive-sdk",
497
+ });
498
+ },
499
+ async executeSend() {
500
+ return Result.ok({
501
+ summary: "Finder follow-up: via sdk",
502
+ output: "sdk follow-up output",
503
+ provider: "sdk-provider",
504
+ model: "sdk-model",
505
+ runtime: "pi-sdk",
506
+ route: "interactive-sdk",
507
+ });
508
+ },
509
+ };
510
+
511
+ const backend = new PiCliTaskExecutionBackend(cliRunner, 1_000, sdkBackend);
512
+ const result = await backend.executeStart({
513
+ taskId: "task_sdk_2",
514
+ subagent: subagentFixture,
515
+ description: "Auth flow scan",
516
+ prompt: "Trace auth validation path",
517
+ cwd: "/tmp/project",
518
+ config: makeConfig("interactive-sdk"),
519
+ signal: undefined,
520
+ });
521
+
522
+ assert.equal(cliCalled, false);
523
+ assert.equal(Result.isOk(result), true);
524
+ if (Result.isError(result)) {
525
+ assert.fail("Expected sdk delegation to succeed");
526
+ }
527
+
528
+ assert.equal(result.value.output, "sdk output");
529
+ assert.equal(result.value.route, "interactive-sdk");
530
+ });
531
+
532
+ defineTest(
533
+ "PiCliTaskExecutionBackend falls back to cli when sdk fails and fallback is enabled",
534
+ async () => {
535
+ const previous = process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI;
536
+ process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI = "true";
537
+
538
+ try {
539
+ let cliCalls = 0;
540
+ const cliRunner: PiCliRunner = async () => {
541
+ cliCalls += 1;
542
+ return {
543
+ exitCode: 0,
544
+ stdout: "cli fallback output",
545
+ stderr: "",
546
+ timedOut: false,
547
+ aborted: false,
548
+ };
549
+ };
550
+
551
+ const sdkBackend: TaskExecutionBackend = {
552
+ id: "interactive-sdk",
553
+ async executeStart() {
554
+ return Result.err(
555
+ new SubagentRuntimeError({
556
+ code: "task_backend_execution_failed",
557
+ stage: "execute_start",
558
+ message: "sdk bootstrap failed",
559
+ }),
560
+ );
561
+ },
562
+ async executeSend() {
563
+ return Result.err(
564
+ new SubagentRuntimeError({
565
+ code: "task_backend_execution_failed",
566
+ stage: "execute_send",
567
+ message: "sdk bootstrap failed",
568
+ }),
569
+ );
570
+ },
571
+ };
572
+
573
+ const backend = new PiCliTaskExecutionBackend(cliRunner, 1_000, sdkBackend);
574
+ const result = await backend.executeStart({
575
+ taskId: "task_sdk_fallback",
576
+ subagent: subagentFixture,
577
+ description: "Fallback run",
578
+ prompt: "Use cli fallback",
579
+ cwd: "/tmp/project",
580
+ config: makeConfig("interactive-sdk"),
581
+ signal: undefined,
582
+ });
583
+
584
+ assert.equal(Result.isOk(result), true);
585
+ if (Result.isError(result)) {
586
+ assert.fail("Expected sdk->cli fallback success");
587
+ }
588
+
589
+ assert.equal(cliCalls, 1);
590
+ assert.equal(result.value.output, "cli fallback output");
591
+ assert.equal(result.value.route, "interactive-shell");
592
+ } finally {
593
+ if (previous === undefined) {
594
+ delete process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI;
595
+ } else {
596
+ process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI = previous;
597
+ }
598
+ }
599
+ },
600
+ );
601
+
602
+ defineTest(
603
+ "PiCliTaskExecutionBackend keeps sdk error taxonomy when fallback is disabled",
604
+ async () => {
605
+ const previous = process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI;
606
+ delete process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI;
607
+
608
+ try {
609
+ let cliCalls = 0;
610
+ const cliRunner: PiCliRunner = async () => {
611
+ cliCalls += 1;
612
+ return {
613
+ exitCode: 0,
614
+ stdout: "cli output",
615
+ stderr: "",
616
+ timedOut: false,
617
+ aborted: false,
618
+ };
619
+ };
620
+
621
+ const sdkBackend: TaskExecutionBackend = {
622
+ id: "interactive-sdk",
623
+ async executeStart() {
624
+ return Result.err(
625
+ new SubagentRuntimeError({
626
+ code: "task_backend_timeout",
627
+ stage: "execute_start",
628
+ message: "sdk timed out",
629
+ }),
630
+ );
631
+ },
632
+ async executeSend() {
633
+ return Result.err(
634
+ new SubagentRuntimeError({
635
+ code: "task_aborted",
636
+ stage: "execute_send",
637
+ message: "sdk aborted",
638
+ }),
639
+ );
640
+ },
641
+ };
642
+
643
+ const backend = new PiCliTaskExecutionBackend(cliRunner, 1_000, sdkBackend);
644
+ const started = await backend.executeStart({
645
+ taskId: "task_sdk_no_fallback",
646
+ subagent: subagentFixture,
647
+ description: "No fallback start",
648
+ prompt: "start",
649
+ cwd: "/tmp/project",
650
+ config: makeConfig("interactive-sdk"),
651
+ signal: undefined,
652
+ });
653
+
654
+ assert.equal(Result.isError(started), true);
655
+ if (Result.isOk(started)) {
656
+ assert.fail("Expected sdk start failure without fallback");
657
+ }
658
+ assert.equal(started.error.code, "task_backend_timeout");
659
+
660
+ const sent = await backend.executeSend({
661
+ taskId: "task_sdk_no_fallback",
662
+ subagent: subagentFixture,
663
+ description: "No fallback send",
664
+ initialPrompt: "initial",
665
+ followUpPrompts: [],
666
+ prompt: "send",
667
+ cwd: "/tmp/project",
668
+ config: makeConfig("interactive-sdk"),
669
+ signal: undefined,
670
+ });
671
+
672
+ assert.equal(Result.isError(sent), true);
673
+ if (Result.isOk(sent)) {
674
+ assert.fail("Expected sdk send failure without fallback");
675
+ }
676
+ assert.equal(sent.error.code, "task_aborted");
677
+ assert.equal(cliCalls, 0);
678
+ } finally {
679
+ if (previous === undefined) {
680
+ delete process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI;
681
+ } else {
682
+ process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI = previous;
683
+ }
684
+ }
685
+ },
686
+ );
687
+
241
688
  defineTest("PiCliTaskExecutionBackend reports timeout and backend execution failures", async () => {
242
689
  const timeoutRunner: PiCliRunner = async () => ({
243
690
  exitCode: 124,
@@ -290,6 +737,58 @@ defineTest("PiCliTaskExecutionBackend reports timeout and backend execution fail
290
737
  assert.equal(failed.error.code, "task_backend_execution_failed");
291
738
  });
292
739
 
740
+ defineTest("PiSdkTaskExecutionBackend maps send timeout and abort", async () => {
741
+ const timeoutBackend = new PiSdkTaskExecutionBackend(async () => ({
742
+ output: "",
743
+ events: [],
744
+ timedOut: true,
745
+ aborted: false,
746
+ }));
747
+
748
+ const timedOut = await timeoutBackend.executeSend({
749
+ taskId: "task_sdk_send_timeout",
750
+ subagent: subagentFixture,
751
+ description: "send timeout",
752
+ initialPrompt: "initial",
753
+ followUpPrompts: [],
754
+ prompt: "follow-up",
755
+ cwd: "/tmp/project",
756
+ config: makeConfig("interactive-sdk"),
757
+ signal: undefined,
758
+ });
759
+
760
+ assert.equal(Result.isError(timedOut), true);
761
+ if (Result.isOk(timedOut)) {
762
+ assert.fail("Expected send timeout error");
763
+ }
764
+ assert.equal(timedOut.error.code, "task_backend_timeout");
765
+
766
+ const abortedBackend = new PiSdkTaskExecutionBackend(async () => ({
767
+ output: "",
768
+ events: [],
769
+ timedOut: false,
770
+ aborted: true,
771
+ }));
772
+
773
+ const aborted = await abortedBackend.executeSend({
774
+ taskId: "task_sdk_send_abort",
775
+ subagent: subagentFixture,
776
+ description: "send abort",
777
+ initialPrompt: "initial",
778
+ followUpPrompts: [],
779
+ prompt: "follow-up",
780
+ cwd: "/tmp/project",
781
+ config: makeConfig("interactive-sdk"),
782
+ signal: undefined,
783
+ });
784
+
785
+ assert.equal(Result.isError(aborted), true);
786
+ if (Result.isOk(aborted)) {
787
+ assert.fail("Expected send aborted error");
788
+ }
789
+ assert.equal(aborted.error.code, "task_aborted");
790
+ });
791
+
293
792
  defineTest("PiCliTaskExecutionBackend resolves backend IDs from runtime config", () => {
294
793
  const backend = new PiCliTaskExecutionBackend(async () => ({
295
794
  exitCode: 0,
@@ -300,6 +799,7 @@ defineTest("PiCliTaskExecutionBackend resolves backend IDs from runtime config",
300
799
  }));
301
800
 
302
801
  assert.equal(backend.resolveBackendId(makeConfig("interactive-shell")), "interactive-shell");
802
+ assert.equal(backend.resolveBackendId(makeConfig("interactive-sdk")), "interactive-sdk");
303
803
  assert.equal(backend.resolveBackendId(makeConfig("none")), "scaffold");
304
804
  assert.equal(backend.resolveBackendId(makeConfig("custom-plugin")), "custom-plugin");
305
805
  });