@pi-ohm/subagents 0.6.4-dev.22211394450.1.e53c4ee → 0.6.4-dev.22232376962.1.01704bd
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/package.json +3 -3
- package/src/runtime/backend/factory.test.ts +12 -0
- package/src/runtime/backend/index.ts +43 -0
- package/src/runtime/backend/model-selection.test.ts +23 -0
- package/src/runtime/backend/model-selection.ts +117 -0
- package/src/runtime/{backend.test.ts → backend/pi-cli-backend.test.ts} +3 -3
- package/src/runtime/backend/pi-cli-backend.ts +359 -0
- package/src/runtime/backend/pi-sdk-backend.test.ts +73 -0
- package/src/runtime/backend/pi-sdk-backend.ts +274 -0
- package/src/runtime/backend/prompts.ts +45 -0
- package/src/runtime/backend/runners.ts +469 -0
- package/src/runtime/backend/scaffold-backend.test.ts +62 -0
- package/src/runtime/backend/scaffold-backend.ts +80 -0
- package/src/runtime/backend/sdk-stream-capture.test.ts +34 -0
- package/src/runtime/backend/sdk-stream-capture.ts +106 -0
- package/src/runtime/backend/system-prompts.test.ts +61 -0
- package/src/runtime/backend/system-prompts.ts +213 -0
- package/src/runtime/backend/types.ts +132 -0
- package/src/runtime/backend.ts +1 -1352
- package/src/runtime/task-transcript.test.ts +109 -0
- package/src/runtime/task-transcript.ts +352 -0
- package/src/runtime/tasks/events.test.ts +66 -0
- package/src/runtime/tasks/index.ts +17 -0
- package/src/runtime/tasks/persistence.test.ts +33 -0
- package/src/runtime/tasks/persistence.ts +467 -0
- package/src/runtime/tasks/policies.test.ts +19 -0
- package/src/runtime/tasks/state-machine.ts +11 -0
- package/src/runtime/{tasks.test.ts → tasks/store.test.ts} +2 -2
- package/src/runtime/tasks/store.ts +839 -0
- package/src/runtime/tasks/types.ts +154 -0
- package/src/runtime/tasks.ts +1 -1413
- package/src/runtime/ui.test.ts +0 -123
- package/src/runtime/ui.ts +10 -325
- package/src/schema/index.ts +27 -0
- package/src/schema/runtime-config.test.ts +44 -0
- package/src/schema/runtime-config.ts +65 -0
- package/src/schema/shared.ts +50 -0
- package/src/schema/task-record.test.ts +41 -0
- package/src/schema/task-record.ts +75 -0
- package/src/{schema.test.ts → schema/task-tool.test.ts} +6 -6
- package/src/schema/task-tool.ts +268 -0
- package/src/schema.ts +1 -446
- package/src/tools/task/contracts.ts +141 -0
- package/src/tools/task/defaults.ts +100 -0
- package/src/tools/task/execution/batch.ts +392 -0
- package/src/tools/task/execution/cancel.ts +82 -0
- package/src/tools/task/execution/index.ts +6 -0
- package/src/tools/task/execution/lifecycle.ts +566 -0
- package/src/tools/task/execution/projection.ts +149 -0
- package/src/tools/task/execution/send.ts +287 -0
- package/src/tools/task/execution/shared.ts +413 -0
- package/src/tools/task/execution/start.ts +127 -0
- package/src/tools/task/execution/status.ts +37 -0
- package/src/tools/task/execution/wait.ts +207 -0
- package/src/tools/task/execution.ts +9 -0
- package/src/tools/task/formatting.test.ts +34 -0
- package/src/tools/task/index.ts +17 -0
- package/src/tools/task/operations.batch.test.ts +117 -0
- package/src/tools/{task.test.ts → task/operations.lifecycle.test.ts} +5 -32
- package/src/tools/task/operations.start.test.ts +118 -0
- package/src/tools/task/operations.ts +218 -0
- package/src/tools/task/registration.test.ts +29 -0
- package/src/tools/task/render.ts +613 -0
- package/src/tools/task/test-fixtures.ts +25 -0
- package/src/tools/task/updates.test.ts +124 -0
- package/src/tools/task/updates.ts +300 -0
- package/src/tools/task.ts +1 -3443
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-ohm/subagents",
|
|
3
|
-
"version": "0.6.4-dev.
|
|
3
|
+
"version": "0.6.4-dev.22232376962.1.01704bd",
|
|
4
4
|
"homepage": "https://github.com/pi-ohm/pi-ohm/tree/dev/packages/subagents#readme",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@mariozechner/pi-coding-agent": "catalog:pi",
|
|
23
|
-
"@pi-ohm/config": "0.6.4-dev.
|
|
24
|
-
"@pi-ohm/tui": "0.6.4-dev.
|
|
23
|
+
"@pi-ohm/config": "0.6.4-dev.22232376962.1.01704bd",
|
|
24
|
+
"@pi-ohm/tui": "0.6.4-dev.22232376962.1.01704bd",
|
|
25
25
|
"better-result": "catalog:",
|
|
26
26
|
"zod": "catalog:"
|
|
27
27
|
},
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createDefaultTaskExecutionBackend } from "./index";
|
|
4
|
+
|
|
5
|
+
function defineTest(name: string, run: () => void | Promise<void>): void {
|
|
6
|
+
void test(name, run);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
defineTest("backend factory defaults to interactive-sdk route", () => {
|
|
10
|
+
const backend = createDefaultTaskExecutionBackend();
|
|
11
|
+
assert.equal(backend.id, "interactive-sdk");
|
|
12
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { PiCliTaskExecutionBackend } from "./pi-cli-backend";
|
|
2
|
+
import type { TaskExecutionBackend } from "./types";
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
ParseSubagentModelSelectionResult,
|
|
6
|
+
ParsedSubagentModelSelection,
|
|
7
|
+
PiCliRunner,
|
|
8
|
+
PiCliRunnerInput,
|
|
9
|
+
PiCliRunnerResult,
|
|
10
|
+
PiSdkRunner,
|
|
11
|
+
PiSdkRunnerInput,
|
|
12
|
+
PiSdkRunnerResult,
|
|
13
|
+
TaskBackendSendInput,
|
|
14
|
+
TaskBackendSendOutput,
|
|
15
|
+
TaskBackendStartInput,
|
|
16
|
+
TaskBackendStartOutput,
|
|
17
|
+
TaskExecutionBackend,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
export { parseSubagentModelSelection } from "./model-selection";
|
|
21
|
+
export {
|
|
22
|
+
applyPiSdkSessionEvent,
|
|
23
|
+
createPiSdkStreamCaptureState,
|
|
24
|
+
finalizePiSdkStreamCapture,
|
|
25
|
+
} from "./sdk-stream-capture";
|
|
26
|
+
export { buildSendPrompt, buildStartPrompt, truncate } from "./prompts";
|
|
27
|
+
export {
|
|
28
|
+
CLI_BACKEND_ROUTE,
|
|
29
|
+
PI_CLI_TOOLS,
|
|
30
|
+
SDK_BACKEND_ROUTE,
|
|
31
|
+
SDK_BACKEND_RUNTIME,
|
|
32
|
+
normalizeOutput,
|
|
33
|
+
runPiCliPrompt,
|
|
34
|
+
runPiSdkPrompt,
|
|
35
|
+
sanitizeNestedOutput,
|
|
36
|
+
} from "./runners";
|
|
37
|
+
export { ScaffoldTaskExecutionBackend } from "./scaffold-backend";
|
|
38
|
+
export { PiCliTaskExecutionBackend, resolveBackendTimeoutMs } from "./pi-cli-backend";
|
|
39
|
+
export { PiSdkTaskExecutionBackend } from "./pi-sdk-backend";
|
|
40
|
+
|
|
41
|
+
export function createDefaultTaskExecutionBackend(): TaskExecutionBackend {
|
|
42
|
+
return new PiCliTaskExecutionBackend();
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { parseSubagentModelSelection } from "./model-selection";
|
|
4
|
+
|
|
5
|
+
function defineTest(name: string, run: () => void | Promise<void>): void {
|
|
6
|
+
void test(name, run);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
defineTest("model-selection parses provider/model with thinking suffix", () => {
|
|
10
|
+
const parsed = parseSubagentModelSelection({
|
|
11
|
+
modelPattern: "openai-codex/gpt-5.3-codex:medium",
|
|
12
|
+
hasModel: (provider, modelId) => provider === "openai-codex" && modelId === "gpt-5.3-codex",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
assert.equal(parsed.ok, true);
|
|
16
|
+
if (!parsed.ok) {
|
|
17
|
+
assert.fail("Expected model parse success");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
assert.equal(parsed.value.provider, "openai-codex");
|
|
21
|
+
assert.equal(parsed.value.modelId, "gpt-5.3-codex");
|
|
22
|
+
assert.equal(parsed.value.thinkingLevel, "medium");
|
|
23
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
export interface ParsedSubagentModelSelection {
|
|
4
|
+
readonly provider: string;
|
|
5
|
+
readonly modelId: string;
|
|
6
|
+
readonly thinkingLevel?: ThinkingLevel;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ParseSubagentModelSelectionResult =
|
|
10
|
+
| {
|
|
11
|
+
readonly ok: true;
|
|
12
|
+
readonly value: ParsedSubagentModelSelection;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
readonly ok: false;
|
|
16
|
+
readonly reason: "invalid_format" | "invalid_thinking_level" | "model_not_found";
|
|
17
|
+
readonly message: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function parseProviderModelPattern(
|
|
21
|
+
modelPattern: string,
|
|
22
|
+
): { readonly provider: string; readonly modelId: string } | undefined {
|
|
23
|
+
const trimmed = modelPattern.trim();
|
|
24
|
+
if (trimmed.length === 0) return undefined;
|
|
25
|
+
|
|
26
|
+
const slashIndex = trimmed.indexOf("/");
|
|
27
|
+
if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) return undefined;
|
|
28
|
+
|
|
29
|
+
const provider = trimmed.slice(0, slashIndex).trim().toLowerCase();
|
|
30
|
+
const modelId = trimmed.slice(slashIndex + 1).trim();
|
|
31
|
+
if (provider.length === 0 || modelId.length === 0) return undefined;
|
|
32
|
+
|
|
33
|
+
return { provider, modelId };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
37
|
+
return (
|
|
38
|
+
value === "off" ||
|
|
39
|
+
value === "minimal" ||
|
|
40
|
+
value === "low" ||
|
|
41
|
+
value === "medium" ||
|
|
42
|
+
value === "high" ||
|
|
43
|
+
value === "xhigh"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseSubagentModelSelection(input: {
|
|
48
|
+
readonly modelPattern: string;
|
|
49
|
+
readonly hasModel: (provider: string, modelId: string) => boolean;
|
|
50
|
+
}): ParseSubagentModelSelectionResult {
|
|
51
|
+
const parsedBase = parseProviderModelPattern(input.modelPattern);
|
|
52
|
+
if (!parsedBase) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: "invalid_format",
|
|
56
|
+
message: `Invalid subagent model '${input.modelPattern}'. Expected '<provider>/<model>' or '<provider>/<model>:<thinking>'.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fullModelId = parsedBase.modelId;
|
|
61
|
+
if (input.hasModel(parsedBase.provider, fullModelId)) {
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
value: {
|
|
65
|
+
provider: parsedBase.provider,
|
|
66
|
+
modelId: fullModelId,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const colonIndex = fullModelId.lastIndexOf(":");
|
|
72
|
+
if (colonIndex <= 0 || colonIndex >= fullModelId.length - 1) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
reason: "model_not_found",
|
|
76
|
+
message: `Configured subagent model '${input.modelPattern}' was not found.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const thinkingRaw = fullModelId
|
|
81
|
+
.slice(colonIndex + 1)
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase();
|
|
84
|
+
if (!isThinkingLevel(thinkingRaw)) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: "invalid_thinking_level",
|
|
88
|
+
message: `Invalid subagent thinking level '${thinkingRaw}' in '${input.modelPattern}'.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const modelId = fullModelId.slice(0, colonIndex).trim();
|
|
93
|
+
if (modelId.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: "invalid_format",
|
|
97
|
+
message: `Invalid subagent model '${input.modelPattern}'. Expected '<provider>/<model>' or '<provider>/<model>:<thinking>'.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!input.hasModel(parsedBase.provider, modelId)) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: "model_not_found",
|
|
105
|
+
message: `Configured subagent model '${input.modelPattern}' was not found.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
value: {
|
|
112
|
+
provider: parsedBase.provider,
|
|
113
|
+
modelId,
|
|
114
|
+
thinkingLevel: thinkingRaw,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -2,8 +2,8 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import type { OhmRuntimeConfig, OhmSubagentBackend } from "@pi-ohm/config";
|
|
4
4
|
import { Result } from "better-result";
|
|
5
|
-
import type { OhmSubagentDefinition } from "
|
|
6
|
-
import { SubagentRuntimeError } from "
|
|
5
|
+
import type { OhmSubagentDefinition } from "../../catalog";
|
|
6
|
+
import { SubagentRuntimeError } from "../../errors";
|
|
7
7
|
import {
|
|
8
8
|
applyPiSdkSessionEvent,
|
|
9
9
|
createDefaultTaskExecutionBackend,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type PiCliRunner,
|
|
17
17
|
type PiSdkRunner,
|
|
18
18
|
type TaskExecutionBackend,
|
|
19
|
-
} from "./
|
|
19
|
+
} from "./index";
|
|
20
20
|
|
|
21
21
|
function defineTest(name: string, run: () => void | Promise<void>): void {
|
|
22
22
|
void test(name, run);
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import { getSubagentConfiguredModel, type OhmRuntimeConfig } from "@pi-ohm/config";
|
|
3
|
+
import type { OhmSubagentDefinition } from "../../catalog";
|
|
4
|
+
import { SubagentRuntimeError, type SubagentResult } from "../../errors";
|
|
5
|
+
import { buildSendPrompt, buildStartPrompt, truncate } from "./prompts";
|
|
6
|
+
import {
|
|
7
|
+
CLI_BACKEND_ROUTE,
|
|
8
|
+
normalizeOutput,
|
|
9
|
+
runPiCliPrompt,
|
|
10
|
+
sanitizeNestedOutput,
|
|
11
|
+
SDK_BACKEND_ROUTE,
|
|
12
|
+
} from "./runners";
|
|
13
|
+
import { PiSdkTaskExecutionBackend } from "./pi-sdk-backend";
|
|
14
|
+
import { ScaffoldTaskExecutionBackend } from "./scaffold-backend";
|
|
15
|
+
import type {
|
|
16
|
+
PiCliRunner,
|
|
17
|
+
TaskBackendSendInput,
|
|
18
|
+
TaskBackendSendOutput,
|
|
19
|
+
TaskBackendStartInput,
|
|
20
|
+
TaskBackendStartOutput,
|
|
21
|
+
TaskExecutionBackend,
|
|
22
|
+
} from "./types";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_BACKEND_TIMEOUT_MS = 180_000;
|
|
25
|
+
const DEFAULT_LIBRARIAN_TIMEOUT_MS = 300_000;
|
|
26
|
+
const DEFAULT_ORACLE_TIMEOUT_MS = 3_600_000;
|
|
27
|
+
|
|
28
|
+
function parsePositiveInteger(raw: string | undefined): number | undefined {
|
|
29
|
+
if (!raw) return undefined;
|
|
30
|
+
const parsed = Number.parseInt(raw, 10);
|
|
31
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getTimeoutMsFromEnv(): number {
|
|
36
|
+
return (
|
|
37
|
+
parsePositiveInteger(process.env.OHM_SUBAGENTS_BACKEND_TIMEOUT_MS) ?? DEFAULT_BACKEND_TIMEOUT_MS
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getSubagentTimeoutMsFromEnv(subagentId: string): number | undefined {
|
|
42
|
+
const key = `OHM_SUBAGENTS_BACKEND_TIMEOUT_MS_${subagentId.trim().toUpperCase()}`;
|
|
43
|
+
return parsePositiveInteger(process.env[key]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveBackendTimeoutMs(input: {
|
|
47
|
+
readonly fallbackTimeoutMs: number;
|
|
48
|
+
readonly subagent: OhmSubagentDefinition;
|
|
49
|
+
}): number {
|
|
50
|
+
const fromSubagentEnv = getSubagentTimeoutMsFromEnv(input.subagent.id);
|
|
51
|
+
if (fromSubagentEnv !== undefined) return fromSubagentEnv;
|
|
52
|
+
|
|
53
|
+
const fromGlobalEnv = getTimeoutMsFromEnv();
|
|
54
|
+
const base = fromGlobalEnv > 0 ? fromGlobalEnv : input.fallbackTimeoutMs;
|
|
55
|
+
|
|
56
|
+
if (input.subagent.id === "librarian") {
|
|
57
|
+
return Math.max(base, DEFAULT_LIBRARIAN_TIMEOUT_MS);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (input.subagent.id === "oracle") {
|
|
61
|
+
return Math.max(base, DEFAULT_ORACLE_TIMEOUT_MS);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return base;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isSdkFallbackToCliEnabled(): boolean {
|
|
68
|
+
const raw = process.env.OHM_SUBAGENTS_SDK_FALLBACK_TO_CLI;
|
|
69
|
+
if (!raw) return false;
|
|
70
|
+
|
|
71
|
+
const normalized = raw.trim().toLowerCase();
|
|
72
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toAbortedError(
|
|
76
|
+
taskId: string,
|
|
77
|
+
stage: "execute_start" | "execute_send",
|
|
78
|
+
): SubagentRuntimeError {
|
|
79
|
+
return new SubagentRuntimeError({
|
|
80
|
+
code: "task_aborted",
|
|
81
|
+
stage,
|
|
82
|
+
message: `Task ${taskId} was aborted before execution`,
|
|
83
|
+
meta: { taskId },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toUnsupportedBackendError(
|
|
88
|
+
taskId: string,
|
|
89
|
+
stage: "execute_start" | "execute_send",
|
|
90
|
+
): SubagentRuntimeError {
|
|
91
|
+
return new SubagentRuntimeError({
|
|
92
|
+
code: "unsupported_subagent_backend",
|
|
93
|
+
stage,
|
|
94
|
+
message: "Configured subagent backend is not implemented in @pi-ohm/subagents",
|
|
95
|
+
meta: { taskId, backend: "custom-plugin" },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toBackendTimeoutError(input: {
|
|
100
|
+
readonly taskId: string;
|
|
101
|
+
readonly stage: "execute_start" | "execute_send";
|
|
102
|
+
readonly timeoutMs: number;
|
|
103
|
+
readonly subagentId: string;
|
|
104
|
+
readonly modelPattern: string | undefined;
|
|
105
|
+
}): SubagentRuntimeError {
|
|
106
|
+
const timeoutSeconds = Math.max(1, Math.round(input.timeoutMs / 1000));
|
|
107
|
+
const modelHint = input.modelPattern ? ` (model: ${input.modelPattern})` : "";
|
|
108
|
+
const guidance =
|
|
109
|
+
input.subagentId === "oracle"
|
|
110
|
+
? "Narrow oracle task scope/context/files or raise OHM_SUBAGENTS_BACKEND_TIMEOUT_MS_ORACLE."
|
|
111
|
+
: `Narrow task scope or raise OHM_SUBAGENTS_BACKEND_TIMEOUT_MS_${input.subagentId.toUpperCase()} (or OHM_SUBAGENTS_BACKEND_TIMEOUT_MS).`;
|
|
112
|
+
|
|
113
|
+
return new SubagentRuntimeError({
|
|
114
|
+
code: "task_backend_timeout",
|
|
115
|
+
stage: input.stage,
|
|
116
|
+
message: `Task ${input.taskId} timed out after ${timeoutSeconds}s while waiting for '${input.subagentId}' backend response${modelHint}. ${guidance}`,
|
|
117
|
+
meta: {
|
|
118
|
+
taskId: input.taskId,
|
|
119
|
+
timeoutMs: input.timeoutMs,
|
|
120
|
+
timeoutSeconds,
|
|
121
|
+
subagentId: input.subagentId,
|
|
122
|
+
modelPattern: input.modelPattern,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toBackendExecutionError(input: {
|
|
128
|
+
readonly taskId: string;
|
|
129
|
+
readonly stage: "execute_start" | "execute_send";
|
|
130
|
+
readonly exitCode: number;
|
|
131
|
+
readonly stderr: string;
|
|
132
|
+
}): SubagentRuntimeError {
|
|
133
|
+
const errorText = input.stderr.trim();
|
|
134
|
+
const summary =
|
|
135
|
+
errorText.length > 0 ? truncate(errorText, 260) : "subagent backend exited non-zero";
|
|
136
|
+
|
|
137
|
+
return new SubagentRuntimeError({
|
|
138
|
+
code: "task_backend_execution_failed",
|
|
139
|
+
stage: input.stage,
|
|
140
|
+
message: `Task ${input.taskId} backend failed: ${summary}`,
|
|
141
|
+
meta: {
|
|
142
|
+
taskId: input.taskId,
|
|
143
|
+
exitCode: input.exitCode,
|
|
144
|
+
stderr: errorText,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveSubagentModelPattern(input: {
|
|
150
|
+
readonly config: OhmRuntimeConfig;
|
|
151
|
+
readonly subagent: OhmSubagentDefinition;
|
|
152
|
+
}): string | undefined {
|
|
153
|
+
return getSubagentConfiguredModel(input.config, input.subagent.id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class PiCliTaskExecutionBackend implements TaskExecutionBackend {
|
|
157
|
+
readonly id = SDK_BACKEND_ROUTE;
|
|
158
|
+
private readonly scaffoldBackend = new ScaffoldTaskExecutionBackend();
|
|
159
|
+
|
|
160
|
+
constructor(
|
|
161
|
+
private readonly runner: PiCliRunner = runPiCliPrompt,
|
|
162
|
+
private readonly timeoutMs: number = getTimeoutMsFromEnv(),
|
|
163
|
+
private readonly sdkBackend: TaskExecutionBackend = new PiSdkTaskExecutionBackend(),
|
|
164
|
+
) {}
|
|
165
|
+
|
|
166
|
+
resolveBackendId(config: OhmRuntimeConfig): string {
|
|
167
|
+
if (config.subagentBackend === "none") return this.scaffoldBackend.id;
|
|
168
|
+
if (config.subagentBackend === "interactive-shell") return CLI_BACKEND_ROUTE;
|
|
169
|
+
if (config.subagentBackend === "interactive-sdk") return SDK_BACKEND_ROUTE;
|
|
170
|
+
if (config.subagentBackend === "custom-plugin") return "custom-plugin";
|
|
171
|
+
return this.id;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private shouldFallbackToCli(error: SubagentRuntimeError): boolean {
|
|
175
|
+
if (!isSdkFallbackToCliEnabled()) return false;
|
|
176
|
+
if (error.code !== "task_backend_execution_failed") return false;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async executeCliStart(
|
|
181
|
+
input: TaskBackendStartInput,
|
|
182
|
+
): Promise<SubagentResult<TaskBackendStartOutput, SubagentRuntimeError>> {
|
|
183
|
+
const modelPattern = resolveSubagentModelPattern({
|
|
184
|
+
config: input.config,
|
|
185
|
+
subagent: input.subagent,
|
|
186
|
+
});
|
|
187
|
+
const timeoutMs = resolveBackendTimeoutMs({
|
|
188
|
+
fallbackTimeoutMs: this.timeoutMs,
|
|
189
|
+
subagent: input.subagent,
|
|
190
|
+
});
|
|
191
|
+
const run = await this.runner({
|
|
192
|
+
cwd: input.cwd,
|
|
193
|
+
prompt: buildStartPrompt(input),
|
|
194
|
+
modelPattern,
|
|
195
|
+
signal: input.signal,
|
|
196
|
+
timeoutMs,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (run.aborted || input.signal?.aborted) {
|
|
200
|
+
return Result.err(toAbortedError(input.taskId, "execute_start"));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (run.timedOut) {
|
|
204
|
+
return Result.err(
|
|
205
|
+
toBackendTimeoutError({
|
|
206
|
+
taskId: input.taskId,
|
|
207
|
+
stage: "execute_start",
|
|
208
|
+
timeoutMs,
|
|
209
|
+
subagentId: input.subagent.id,
|
|
210
|
+
modelPattern,
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (run.exitCode !== 0) {
|
|
216
|
+
return Result.err(
|
|
217
|
+
toBackendExecutionError({
|
|
218
|
+
taskId: input.taskId,
|
|
219
|
+
stage: "execute_start",
|
|
220
|
+
exitCode: run.exitCode,
|
|
221
|
+
stderr: run.stderr,
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const normalized = sanitizeNestedOutput(normalizeOutput(run.stdout, run.stderr));
|
|
227
|
+
const summary = `${input.subagent.name}: ${truncate(input.description, 72)}`;
|
|
228
|
+
return Result.ok({
|
|
229
|
+
summary,
|
|
230
|
+
output: normalized.output,
|
|
231
|
+
provider: normalized.provider,
|
|
232
|
+
model: normalized.model,
|
|
233
|
+
runtime: normalized.runtime,
|
|
234
|
+
route: CLI_BACKEND_ROUTE,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async executeCliSend(
|
|
239
|
+
input: TaskBackendSendInput,
|
|
240
|
+
): Promise<SubagentResult<TaskBackendSendOutput, SubagentRuntimeError>> {
|
|
241
|
+
const modelPattern = resolveSubagentModelPattern({
|
|
242
|
+
config: input.config,
|
|
243
|
+
subagent: input.subagent,
|
|
244
|
+
});
|
|
245
|
+
const timeoutMs = resolveBackendTimeoutMs({
|
|
246
|
+
fallbackTimeoutMs: this.timeoutMs,
|
|
247
|
+
subagent: input.subagent,
|
|
248
|
+
});
|
|
249
|
+
const run = await this.runner({
|
|
250
|
+
cwd: input.cwd,
|
|
251
|
+
prompt: buildSendPrompt(input),
|
|
252
|
+
modelPattern,
|
|
253
|
+
signal: input.signal,
|
|
254
|
+
timeoutMs,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (run.aborted || input.signal?.aborted) {
|
|
258
|
+
return Result.err(toAbortedError(input.taskId, "execute_send"));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (run.timedOut) {
|
|
262
|
+
return Result.err(
|
|
263
|
+
toBackendTimeoutError({
|
|
264
|
+
taskId: input.taskId,
|
|
265
|
+
stage: "execute_send",
|
|
266
|
+
timeoutMs,
|
|
267
|
+
subagentId: input.subagent.id,
|
|
268
|
+
modelPattern,
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (run.exitCode !== 0) {
|
|
274
|
+
return Result.err(
|
|
275
|
+
toBackendExecutionError({
|
|
276
|
+
taskId: input.taskId,
|
|
277
|
+
stage: "execute_send",
|
|
278
|
+
exitCode: run.exitCode,
|
|
279
|
+
stderr: run.stderr,
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const normalized = sanitizeNestedOutput(normalizeOutput(run.stdout, run.stderr));
|
|
285
|
+
const summary = `${input.subagent.name} follow-up: ${truncate(input.prompt, 72)}`;
|
|
286
|
+
return Result.ok({
|
|
287
|
+
summary,
|
|
288
|
+
output: normalized.output,
|
|
289
|
+
provider: normalized.provider,
|
|
290
|
+
model: normalized.model,
|
|
291
|
+
runtime: normalized.runtime,
|
|
292
|
+
route: CLI_BACKEND_ROUTE,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async executeStart(
|
|
297
|
+
input: TaskBackendStartInput,
|
|
298
|
+
): Promise<SubagentResult<TaskBackendStartOutput, SubagentRuntimeError>> {
|
|
299
|
+
if (input.signal?.aborted) {
|
|
300
|
+
return Result.err(toAbortedError(input.taskId, "execute_start"));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (input.config.subagentBackend === "none") {
|
|
304
|
+
return this.scaffoldBackend.executeStart(input);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (input.config.subagentBackend === "custom-plugin") {
|
|
308
|
+
return Result.err(toUnsupportedBackendError(input.taskId, "execute_start"));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (input.config.subagentBackend === "interactive-shell") {
|
|
312
|
+
return this.executeCliStart(input);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (input.config.subagentBackend === "interactive-sdk") {
|
|
316
|
+
const sdkResult = await this.sdkBackend.executeStart(input);
|
|
317
|
+
if (Result.isOk(sdkResult)) return sdkResult;
|
|
318
|
+
if (!this.shouldFallbackToCli(sdkResult.error)) return sdkResult;
|
|
319
|
+
return this.executeCliStart(input);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const sdkResult = await this.sdkBackend.executeStart(input);
|
|
323
|
+
if (Result.isOk(sdkResult)) return sdkResult;
|
|
324
|
+
if (!this.shouldFallbackToCli(sdkResult.error)) return sdkResult;
|
|
325
|
+
return this.executeCliStart(input);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async executeSend(
|
|
329
|
+
input: TaskBackendSendInput,
|
|
330
|
+
): Promise<SubagentResult<TaskBackendSendOutput, SubagentRuntimeError>> {
|
|
331
|
+
if (input.signal?.aborted) {
|
|
332
|
+
return Result.err(toAbortedError(input.taskId, "execute_send"));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (input.config.subagentBackend === "none") {
|
|
336
|
+
return this.scaffoldBackend.executeSend(input);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (input.config.subagentBackend === "custom-plugin") {
|
|
340
|
+
return Result.err(toUnsupportedBackendError(input.taskId, "execute_send"));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (input.config.subagentBackend === "interactive-shell") {
|
|
344
|
+
return this.executeCliSend(input);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (input.config.subagentBackend === "interactive-sdk") {
|
|
348
|
+
const sdkResult = await this.sdkBackend.executeSend(input);
|
|
349
|
+
if (Result.isOk(sdkResult)) return sdkResult;
|
|
350
|
+
if (!this.shouldFallbackToCli(sdkResult.error)) return sdkResult;
|
|
351
|
+
return this.executeCliSend(input);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const sdkResult = await this.sdkBackend.executeSend(input);
|
|
355
|
+
if (Result.isOk(sdkResult)) return sdkResult;
|
|
356
|
+
if (!this.shouldFallbackToCli(sdkResult.error)) return sdkResult;
|
|
357
|
+
return this.executeCliSend(input);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { OhmRuntimeConfig } from "@pi-ohm/config";
|
|
4
|
+
import { Result } from "better-result";
|
|
5
|
+
import { PiSdkTaskExecutionBackend } from "./pi-sdk-backend";
|
|
6
|
+
|
|
7
|
+
function defineTest(name: string, run: () => void | Promise<void>): void {
|
|
8
|
+
void test(name, run);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const config: OhmRuntimeConfig = {
|
|
12
|
+
defaultMode: "smart",
|
|
13
|
+
subagentBackend: "interactive-sdk",
|
|
14
|
+
features: {
|
|
15
|
+
handoff: true,
|
|
16
|
+
subagents: true,
|
|
17
|
+
sessionThreadSearch: true,
|
|
18
|
+
handoffVisualizer: true,
|
|
19
|
+
painterImagegen: false,
|
|
20
|
+
},
|
|
21
|
+
painter: {
|
|
22
|
+
googleNanoBanana: { enabled: false, model: "" },
|
|
23
|
+
openai: { enabled: false, model: "" },
|
|
24
|
+
azureOpenai: { enabled: false, deployment: "", endpoint: "", apiVersion: "" },
|
|
25
|
+
},
|
|
26
|
+
subagents: {
|
|
27
|
+
taskMaxConcurrency: 2,
|
|
28
|
+
taskRetentionMs: 60_000,
|
|
29
|
+
permissions: {
|
|
30
|
+
default: "allow",
|
|
31
|
+
subagents: {},
|
|
32
|
+
allowInternalRouting: false,
|
|
33
|
+
},
|
|
34
|
+
profiles: {},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
defineTest("pi-sdk-backend executes start with runner", async () => {
|
|
39
|
+
const backend = new PiSdkTaskExecutionBackend(async () => {
|
|
40
|
+
return {
|
|
41
|
+
output: "sdk output",
|
|
42
|
+
events: [],
|
|
43
|
+
timedOut: false,
|
|
44
|
+
aborted: false,
|
|
45
|
+
runtime: "pi-sdk",
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const result = await backend.executeStart({
|
|
50
|
+
taskId: "task_sdk_1",
|
|
51
|
+
subagent: {
|
|
52
|
+
id: "finder",
|
|
53
|
+
name: "Finder",
|
|
54
|
+
summary: "Search specialist",
|
|
55
|
+
whenToUse: ["search"],
|
|
56
|
+
scaffoldPrompt: "search prompt",
|
|
57
|
+
primary: false,
|
|
58
|
+
},
|
|
59
|
+
description: "sdk smoke",
|
|
60
|
+
prompt: "find auth",
|
|
61
|
+
cwd: "/tmp/project",
|
|
62
|
+
config,
|
|
63
|
+
signal: undefined,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.equal(Result.isOk(result), true);
|
|
67
|
+
if (Result.isError(result)) {
|
|
68
|
+
assert.fail("Expected sdk backend success");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
assert.equal(result.value.runtime, "pi-sdk");
|
|
72
|
+
assert.equal(result.value.route, "interactive-sdk");
|
|
73
|
+
});
|