oira666_pi-subagent 0.5.3 → 0.5.4
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/README.md +2 -2
- package/docs/configuration.md +3 -1
- package/extension/expand-command.ts +31 -50
- package/package.json +1 -1
- package/runner/smart-decision.ts +12 -1
- package/ui/expand-view.ts +62 -0
- package/ui/pager-view.ts +3 -2
- package/ui/pager.ts +1 -1
package/README.md
CHANGED
|
@@ -111,9 +111,9 @@ Collapsed results show each child's name, task, status, and most recent activity
|
|
|
111
111
|
| T | Current turn's tool list |
|
|
112
112
|
| Up / Down, Enter | Select and open a tool or nested child |
|
|
113
113
|
| C | Children across all turns |
|
|
114
|
-
| Esc |
|
|
114
|
+
| Esc | Back one level: tool detail, tools/children, parent subagent, then the searchable list. From the list or a directly named subagent, close. |
|
|
115
115
|
| /, n, N | Search, next match, previous match |
|
|
116
|
-
| q | Close |
|
|
116
|
+
| q | Close from any transcript view |
|
|
117
117
|
|
|
118
118
|
While workers run, steering input can be broadcast to selected names, including nested paths such as `John > Maria`. Only inputs marked as streaming `steer` open the routing prompt. Idle prompts and queued follow-ups remain with the parent.
|
|
119
119
|
|
package/docs/configuration.md
CHANGED
|
@@ -63,6 +63,7 @@ Optionally let TypeSafe's Jev choose a model for each subagent task. Add this se
|
|
|
63
63
|
"smart-decision": {
|
|
64
64
|
"enabled": true,
|
|
65
65
|
"fallback": true,
|
|
66
|
+
"timeout_seconds": 1200,
|
|
66
67
|
"model": "jev",
|
|
67
68
|
"provider_url": "https://api.typesafe.ai",
|
|
68
69
|
"api_key": "YOUR_TYPESAFE_API_KEY",
|
|
@@ -80,9 +81,10 @@ Replace the example choices with models available in your Pi installation. Each
|
|
|
80
81
|
|
|
81
82
|
- `enabled` must be `true`. Missing settings, disabled settings, or an empty model list leave existing behavior unchanged and make no Jev requests.
|
|
82
83
|
- `model` selects the decision model, not the worker model. `jev` maps to TypeSafe's `jev-latest`; explicit TypeSafe model IDs also work.
|
|
84
|
+
- `timeout_seconds` defaults to `1200` seconds and bounds the whole Jev request, including reading the response body. Set a number from `0.001` to `2147483.647` seconds. Invalid values follow the `fallback` policy.
|
|
83
85
|
- `provider_url` is the provider base URL, defaulting to `https://api.typesafe.ai`. For a local compatible server, use `http://localhost:8765`. The extension appends `/v1/systemone`; trailing slashes are ignored. Use an HTTP or HTTPS URL without credentials, query parameters, or a fragment.
|
|
84
86
|
- `api_key` is your TypeSafe API key. Worker providers still need their own Pi credentials.
|
|
85
|
-
- `fallback` defaults to `true`. When a loaded `smart-decision` object omits it, the extension adds `"fallback": true` and saves that configuration file at runtime. Existing values, including `false`, are left untouched. Untrusted project files are never changed. If saving fails, a warning is emitted and the runtime default still applies. If selection fails, a short warning is written and the worker uses the original launch settings. Set it to `false` to return a tool error without launching the affected worker. This covers network errors, the
|
|
87
|
+
- `fallback` defaults to `true`. When a loaded `smart-decision` object omits it, the extension adds `"fallback": true` and saves that configuration file at runtime. Existing values, including `false`, are left untouched. Untrusted project files are never changed. If saving fails, a warning is emitted and the runtime default still applies. If selection fails, a short warning is written and the worker uses the original launch settings. Set it to `false` to return a tool error without launching the affected worker. This covers network errors, the configured request timeout, API errors, invalid answers, and invalid enabled configuration. Cancellation cancels the task instead of falling back.
|
|
86
88
|
|
|
87
89
|
Each fresh launch and actual resume sends the agent definition's system prompt, the assigned task, and the candidate descriptions to `<provider_url>/v1/systemone`. Jev must select a configured candidate. When a task refers to a separate task-description or handoff file whose contents were not supplied, Jev is instructed to assume higher complexity and prefer a more capable model or higher reasoning level. It does not read that file. Mentioning a source filename alone does not trigger this guidance. Its provider, model, and thinking level override the parent model and agent frontmatter for that run. The parent's explicit `--provider` and `--api-key` are not forwarded when a Jev choice is applied. Startup retries reuse the choice. Finished results reused without launching a process make no request.
|
|
88
90
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildSubagentDetail, findNameRecord, type SubagentDetail } from "../detail.js";
|
|
2
2
|
import { readNamesRegistry, type NamesRegistry, type SubagentNameRecord } from "../names.js";
|
|
3
|
-
import {
|
|
3
|
+
import { filterPickerItems, type PickerItem } from "../overlay.js";
|
|
4
|
+
import { SubagentExpandView } from "../ui/expand-view.js";
|
|
4
5
|
import type { SessionContext } from "./contracts.js";
|
|
5
6
|
import type { ExtensionState } from "./state.js";
|
|
6
7
|
|
|
@@ -66,63 +67,43 @@ export function registerSubagentExpandCommand(state: ExtensionState): void {
|
|
|
66
67
|
return;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
if (!record) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
const record = name ? findNameRecord(registry, name) : undefined;
|
|
71
|
+
if (!record && name) {
|
|
72
|
+
const suggestions = filterPickerItems(
|
|
73
|
+
sortedRecords.map((item) => ({ name: item.name, agent: item.agent, task: item.task })),
|
|
74
|
+
name,
|
|
75
|
+
)
|
|
76
|
+
.slice(0, 10)
|
|
77
|
+
.map((item) => item.name);
|
|
78
|
+
ctx.ui.notify(
|
|
79
|
+
`Unknown subagent "${name}".${suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : ""}`,
|
|
80
|
+
"error",
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const resolveDetail = (selectedName: string): SubagentDetail | undefined => {
|
|
86
|
+
const selected = findNameRecord(registry, selectedName);
|
|
87
|
+
if (!selected) return undefined;
|
|
88
|
+
try {
|
|
89
|
+
return buildSubagentDetail(selected, { sessionDir: resolveDetailSessionDir(state, selected) });
|
|
90
|
+
} catch (err) {
|
|
78
91
|
ctx.ui.notify(
|
|
79
|
-
`
|
|
92
|
+
`Failed to read subagent session: ${err instanceof Error ? err.message : String(err)}`,
|
|
80
93
|
"error",
|
|
81
94
|
);
|
|
82
|
-
return;
|
|
95
|
+
return undefined;
|
|
83
96
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
(tui, theme, _keybindings, done: (value: string | undefined) => void) =>
|
|
88
|
-
new SubagentPicker({
|
|
89
|
-
items: sortedRecords.map((item) => ({ name: item.name, agent: item.agent, task: item.task })),
|
|
90
|
-
getRows: () => tui?.terminal?.rows ?? 30,
|
|
91
|
-
theme,
|
|
92
|
-
requestRender: () => tui?.requestRender?.(),
|
|
93
|
-
onPick: (value) => done(value),
|
|
94
|
-
}),
|
|
95
|
-
{
|
|
96
|
-
overlay: true,
|
|
97
|
-
overlayOptions: { anchor: "center", width: "80%", maxHeight: "90%", margin: 1 },
|
|
98
|
-
},
|
|
99
|
-
);
|
|
100
|
-
if (!picked) return;
|
|
101
|
-
record = findNameRecord(registry, picked);
|
|
102
|
-
if (!record) return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let detail: SubagentDetail;
|
|
106
|
-
try {
|
|
107
|
-
detail = buildSubagentDetail(record, { sessionDir: resolveDetailSessionDir(state, record) });
|
|
108
|
-
} catch (err) {
|
|
109
|
-
ctx.ui.notify(`Failed to read subagent session: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
97
|
+
};
|
|
98
|
+
const detail = record ? resolveDetail(record.name) : undefined;
|
|
99
|
+
if (record && !detail) return;
|
|
112
100
|
|
|
113
101
|
await ctx.ui.custom(
|
|
114
102
|
(tui, theme, _keybindings, done: (value: void) => void) =>
|
|
115
|
-
new
|
|
103
|
+
new SubagentExpandView({
|
|
104
|
+
items: sortedRecords.map((item) => ({ name: item.name, agent: item.agent, task: item.task })),
|
|
116
105
|
detail,
|
|
117
|
-
resolveDetail
|
|
118
|
-
const childRecord = findNameRecord(registry, childName);
|
|
119
|
-
if (!childRecord) return undefined;
|
|
120
|
-
try {
|
|
121
|
-
return buildSubagentDetail(childRecord, { sessionDir: resolveDetailSessionDir(state, childRecord) });
|
|
122
|
-
} catch {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
},
|
|
106
|
+
resolveDetail,
|
|
126
107
|
getRows: () => tui?.terminal?.rows ?? 30,
|
|
127
108
|
theme,
|
|
128
109
|
requestRender: () => tui?.requestRender?.(),
|
package/package.json
CHANGED
package/runner/smart-decision.ts
CHANGED
|
@@ -2,6 +2,7 @@ type Selection = { provider: string; model: string; thinking: string };
|
|
|
2
2
|
|
|
3
3
|
export interface SmartDecisionConfig {
|
|
4
4
|
readonly fallback: boolean;
|
|
5
|
+
readonly timeoutSeconds: number;
|
|
5
6
|
readonly invalid?: boolean;
|
|
6
7
|
readonly model: string;
|
|
7
8
|
readonly apiKey: string;
|
|
@@ -59,6 +60,7 @@ export function parseSmartDecisionConfig(value: unknown): SmartDecisionConfig |
|
|
|
59
60
|
const invalid = (): SmartDecisionConfig => ({
|
|
60
61
|
fallback: value.fallback !== false,
|
|
61
62
|
invalid: true,
|
|
63
|
+
timeoutSeconds: 1200,
|
|
62
64
|
model: "",
|
|
63
65
|
apiKey: "",
|
|
64
66
|
providerUrl: DEFAULT_PROVIDER_URL,
|
|
@@ -67,6 +69,14 @@ export function parseSmartDecisionConfig(value: unknown): SmartDecisionConfig |
|
|
|
67
69
|
if (!nonempty(value.model) || !nonempty(value.api_key) || !Array.isArray(value.use_models)) return invalid();
|
|
68
70
|
const providerUrl = parseProviderUrl(value.provider_url);
|
|
69
71
|
if (!providerUrl) return invalid();
|
|
72
|
+
const timeoutSeconds = value.timeout_seconds === undefined ? 1200 : value.timeout_seconds;
|
|
73
|
+
if (
|
|
74
|
+
typeof timeoutSeconds !== "number" ||
|
|
75
|
+
!Number.isFinite(timeoutSeconds) ||
|
|
76
|
+
timeoutSeconds < 0.001 ||
|
|
77
|
+
timeoutSeconds > 2_147_483.647
|
|
78
|
+
)
|
|
79
|
+
return invalid();
|
|
70
80
|
const candidates: (Selection & { key: string; description: string })[] = [];
|
|
71
81
|
const seen = new Set<string>();
|
|
72
82
|
let count = 0;
|
|
@@ -86,6 +96,7 @@ export function parseSmartDecisionConfig(value: unknown): SmartDecisionConfig |
|
|
|
86
96
|
const model = value.model.trim();
|
|
87
97
|
return {
|
|
88
98
|
fallback: value.fallback !== false,
|
|
99
|
+
timeoutSeconds,
|
|
89
100
|
model: model === "jev" ? "jev-latest" : model,
|
|
90
101
|
apiKey: value.api_key.trim(),
|
|
91
102
|
providerUrl,
|
|
@@ -109,7 +120,7 @@ export async function selectSmartDecision(
|
|
|
109
120
|
});
|
|
110
121
|
controller.signal.addEventListener("abort", rejectAborted, { once: true });
|
|
111
122
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
112
|
-
const timer = setTimeout(cancel,
|
|
123
|
+
const timer = setTimeout(cancel, config.timeoutSeconds * 1000);
|
|
113
124
|
|
|
114
125
|
try {
|
|
115
126
|
if (config.invalid || !config.candidates.length) throw new Error("Invalid smart-decision settings");
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { DetailTheme, SubagentDetail } from "../detail.js";
|
|
2
|
+
import { SubagentPager } from "./pager.js";
|
|
3
|
+
import { SubagentPicker, type PickerItem } from "./picker.js";
|
|
4
|
+
|
|
5
|
+
export interface ExpandViewOptions {
|
|
6
|
+
items: PickerItem[];
|
|
7
|
+
detail?: SubagentDetail;
|
|
8
|
+
resolveDetail: (name: string) => SubagentDetail | undefined;
|
|
9
|
+
getRows: () => number;
|
|
10
|
+
theme: DetailTheme;
|
|
11
|
+
requestRender: () => void;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Keep the picker alive while browsing transcripts so Esc restores its search and selection. */
|
|
16
|
+
export class SubagentExpandView {
|
|
17
|
+
private readonly picker?: SubagentPicker;
|
|
18
|
+
private pager?: SubagentPager;
|
|
19
|
+
|
|
20
|
+
constructor(private readonly options: ExpandViewOptions) {
|
|
21
|
+
if (options.detail) {
|
|
22
|
+
this.openDetail(options.detail);
|
|
23
|
+
} else {
|
|
24
|
+
this.picker = new SubagentPicker({
|
|
25
|
+
...options,
|
|
26
|
+
onPick: (name) => {
|
|
27
|
+
if (!name) options.onClose();
|
|
28
|
+
else {
|
|
29
|
+
const detail = options.resolveDetail(name);
|
|
30
|
+
if (detail) this.openDetail(detail);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private openDetail(detail: SubagentDetail): void {
|
|
38
|
+
this.pager = new SubagentPager({
|
|
39
|
+
...this.options,
|
|
40
|
+
detail,
|
|
41
|
+
onBack: this.picker
|
|
42
|
+
? () => {
|
|
43
|
+
this.pager = undefined;
|
|
44
|
+
}
|
|
45
|
+
: undefined,
|
|
46
|
+
});
|
|
47
|
+
this.options.requestRender();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
handleInput(data: string): void {
|
|
51
|
+
(this.pager ?? this.picker)?.handleInput(data);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render(width: number): string[] {
|
|
55
|
+
return (this.pager ?? this.picker)?.render(width) ?? [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
invalidate(): void {
|
|
59
|
+
this.picker?.invalidate();
|
|
60
|
+
this.pager?.invalidate();
|
|
61
|
+
}
|
|
62
|
+
}
|
package/ui/pager-view.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface PagerOptions {
|
|
|
23
23
|
theme: DetailTheme;
|
|
24
24
|
requestRender: () => void;
|
|
25
25
|
onClose: () => void;
|
|
26
|
+
onBack?: () => void;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export class PagerView {
|
|
@@ -190,8 +191,8 @@ export class PagerView {
|
|
|
190
191
|
if (this.status) return `${this.status} n/N next/previous • / new search`;
|
|
191
192
|
const position = `${this.scroll + 1}-${Math.min(this.lines.length, this.scroll + this.viewport)}/${this.lines.length}`;
|
|
192
193
|
if (this.mode === "turn") {
|
|
193
|
-
const back = this.parentStack.length > 0 ? "Esc parent" : "Esc close";
|
|
194
|
-
return `${position} ←/→ turn • T tools • C children • / search • ${back}`;
|
|
194
|
+
const back = this.parentStack.length > 0 ? "Esc parent" : this.options.onBack ? "Esc list" : "Esc close";
|
|
195
|
+
return `${position} ←/→ turn • T tools • C children • / search • ${back} • q close`;
|
|
195
196
|
}
|
|
196
197
|
if (this.mode === "tools")
|
|
197
198
|
return `${position} ↑/↓ row • Enter inspect/expand child • / search • Esc back • q close`;
|