@khorsheed/dsh-local-agent 0.2.0-rc.3 → 0.2.0
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/CHANGELOG.md +19 -1
- package/README.en.md +23 -164
- package/README.i18n.yaml +2 -2
- package/README.md +23 -164
- package/lib/client.js +373 -133
- package/lib/index.js +483 -14
- package/lib/member-bridge.js +195 -25
- package/lib/typert.host.js +107 -15
- package/lib/typert.remote-client.js +4 -4
- package/lib/types/client/HarnessModelPicker.d.ts +21 -1
- package/lib/types/client/HarnessModelPicker.js +31 -3
- package/lib/types/client/MemberLiveNode.js +38 -4
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.js +16 -2
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/locales.js +2 -2
- package/lib/types/client/model-directory-scope.d.ts +14 -0
- package/lib/types/client/model-directory-scope.js +20 -0
- package/lib/types/index.d.ts +18 -2
- package/lib/types/index.js +81 -14
- package/lib/types/live-stream.d.ts +11 -0
- package/lib/types/live-stream.js +26 -0
- package/lib/types/member-bridge.d.ts +11 -1
- package/lib/types/member-bridge.js +81 -13
- package/lib/types/member-channel.d.ts +44 -1
- package/lib/types/member-channel.js +65 -2
- package/lib/types/mode-skills.d.ts +18 -0
- package/lib/types/mode-skills.js +56 -0
- package/lib/types/preset-tool-host.d.ts +70 -0
- package/lib/types/preset-tool-host.js +210 -0
- package/lib/types/types.d.ts +27 -2
- package/package.json +5 -1
|
@@ -15,7 +15,7 @@ let _khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_resul
|
|
|
15
15
|
const _khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_result$schema = () => (_khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_result$schema$value ??= z.object({
|
|
16
16
|
'requestId': z.string(),
|
|
17
17
|
'revision': z.number(),
|
|
18
|
-
'status': z.union([z.literal("cancelled"), z.literal("failed"), z.literal("pending"), z.literal("applied"), z.literal("
|
|
18
|
+
'status': z.union([z.literal("cancelled"), z.literal("failed"), z.literal("pending"), z.literal("applied"), z.literal("conflict"), z.literal("unsupported"), z.literal("locked"), z.literal("applying")]),
|
|
19
19
|
'error': z.string().optional(),
|
|
20
20
|
}))
|
|
21
21
|
let _khorsheed_dsh_local_agent_localAgentGateway_checkEndpoint_parameter_0$schema$value
|
|
@@ -1106,7 +1106,7 @@ let _khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_resul
|
|
|
1106
1106
|
const _khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_result$schema = () => (_khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_result$schema$value ??= z.object({
|
|
1107
1107
|
'requestId': z.string(),
|
|
1108
1108
|
'revision': z.number(),
|
|
1109
|
-
'status': z.union([z.literal("cancelled"), z.literal("failed"), z.literal("pending"), z.literal("applied"), z.literal("
|
|
1109
|
+
'status': z.union([z.literal("cancelled"), z.literal("failed"), z.literal("pending"), z.literal("applied"), z.literal("conflict"), z.literal("unsupported"), z.literal("locked"), z.literal("applying")]),
|
|
1110
1110
|
'error': z.string().optional(),
|
|
1111
1111
|
}))
|
|
1112
1112
|
let _khorsheed_dsh_local_agent_localAgentGateway_sessions_parameter_0$schema$value
|
|
@@ -1363,7 +1363,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
|
|
|
1363
1363
|
typeSymbol: '@khorsheed/dsh-local-agent/types#LocalAgentControlReceipt',
|
|
1364
1364
|
create: _khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_result$schema,
|
|
1365
1365
|
schema: _khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_result$schema(),
|
|
1366
|
-
jsonSchema: {"type":"object","properties":{"requestId":{"type":"string"},"revision":{"type":"number"},"status":{"type":"string","enum":["cancelled","failed","pending","applied","
|
|
1366
|
+
jsonSchema: {"type":"object","properties":{"requestId":{"type":"string"},"revision":{"type":"number"},"status":{"type":"string","enum":["cancelled","failed","pending","applied","conflict","unsupported","locked","applying"]},"error":{"type":"string"}},"required":["requestId","revision","status"],"additionalProperties":true},
|
|
1367
1367
|
},
|
|
1368
1368
|
sourceLocation: {"file":"packages/local-agent/src/gateway.ts","line":414,"column":9},
|
|
1369
1369
|
},
|
|
@@ -2399,7 +2399,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
|
|
|
2399
2399
|
typeSymbol: '@khorsheed/dsh-local-agent/types#LocalAgentControlReceipt',
|
|
2400
2400
|
create: _khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_result$schema,
|
|
2401
2401
|
schema: _khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_result$schema(),
|
|
2402
|
-
jsonSchema: {"type":"object","properties":{"requestId":{"type":"string"},"revision":{"type":"number"},"status":{"type":"string","enum":["cancelled","failed","pending","applied","
|
|
2402
|
+
jsonSchema: {"type":"object","properties":{"requestId":{"type":"string"},"revision":{"type":"number"},"status":{"type":"string","enum":["cancelled","failed","pending","applied","conflict","unsupported","locked","applying"]},"error":{"type":"string"}},"required":["requestId","revision","status"],"additionalProperties":true},
|
|
2403
2403
|
},
|
|
2404
2404
|
sourceLocation: {"file":"packages/local-agent/src/gateway.ts","line":409,"column":9},
|
|
2405
2405
|
},
|
|
@@ -8,10 +8,30 @@ export interface HarnessModelPickerProps {
|
|
|
8
8
|
face: ModelDirectoryFace;
|
|
9
9
|
value: string;
|
|
10
10
|
onChange(value: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Present = the menu also offers the model's reasoning-effort row
|
|
13
|
+
* ('' = follow the harness default). Absent = the picker stays model-only
|
|
14
|
+
* (settings cards' behavior).
|
|
15
|
+
*/
|
|
16
|
+
effort?: {
|
|
17
|
+
value: string;
|
|
18
|
+
onChange(value: string): void;
|
|
19
|
+
} | undefined;
|
|
11
20
|
disabled?: boolean | undefined;
|
|
12
21
|
defaultLabel?: string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Present = render as form fields (a labelled model select, plus an effort
|
|
24
|
+
* select when `effort` is present) in the host form's own classes, instead
|
|
25
|
+
* of the drill-down menu — for a dialog whose other fields are selects.
|
|
26
|
+
*/
|
|
27
|
+
fields?: {
|
|
28
|
+
field?: string | undefined;
|
|
29
|
+
label?: string | undefined;
|
|
30
|
+
select?: string | undefined;
|
|
31
|
+
row?: string | undefined;
|
|
32
|
+
} | undefined;
|
|
13
33
|
t: ConfigurationTranslate;
|
|
14
34
|
}
|
|
15
35
|
/** Discover while expanded or resolving an empty choice; settings remain caller-owned drafts. */
|
|
16
|
-
export declare function HarnessModelPicker({ face, value, onChange, disabled, t }: HarnessModelPickerProps): import("react").JSX.Element;
|
|
36
|
+
export declare function HarnessModelPicker({ face, value, onChange, effort, disabled, fields, t }: HarnessModelPickerProps): import("react").JSX.Element;
|
|
17
37
|
//# sourceMappingURL=HarnessModelPicker.d.ts.map
|
|
@@ -4,9 +4,11 @@ import { ModelConfigurationFields } from './ModelConfigurationFields.js';
|
|
|
4
4
|
import css from './MemberConfiguration.module.css';
|
|
5
5
|
import { useConfigurationMenu } from './use-configuration-menu.js';
|
|
6
6
|
/** Discover while expanded or resolving an empty choice; settings remain caller-owned drafts. */
|
|
7
|
-
export function HarnessModelPicker({ face, value, onChange, disabled, t }) {
|
|
7
|
+
export function HarnessModelPicker({ face, value, onChange, effort, disabled, fields, t }) {
|
|
8
8
|
const menu = useConfigurationMenu('below');
|
|
9
|
-
const {
|
|
9
|
+
const { setOpen } = menu;
|
|
10
|
+
// Form fields show the directory all the time: discovery runs as if open.
|
|
11
|
+
const open = menu.open || fields !== undefined;
|
|
10
12
|
const [surface, setSurface] = useState();
|
|
11
13
|
const directory = surface?.face === face ? surface.directory : undefined;
|
|
12
14
|
const currentFace = useRef(face);
|
|
@@ -44,5 +46,31 @@ export function HarnessModelPicker({ face, value, onChange, disabled, t }) {
|
|
|
44
46
|
onChange(first.value);
|
|
45
47
|
}, [directory, value, disabled, onChange]);
|
|
46
48
|
const entry = directory?.entries.find(entry => entry.value === value);
|
|
47
|
-
|
|
49
|
+
if (fields !== undefined)
|
|
50
|
+
return _jsx(ModelSelects, { directory: directory, error: error, value: value, onChange: onChange, effort: effort, disabled: disabled, classes: fields, t: t });
|
|
51
|
+
return _jsxs("details", { ref: menu.root, className: `${css.root} ${css.settings}`, open: open, onToggle: event => setOpen(event.currentTarget.open), children: [_jsx("summary", { "aria-label": t('configuration.model'), "aria-expanded": open, children: _jsx("span", { className: css.triggerLabel, children: entry?.label ?? (value || t('configuration.choose')) }) }), open && _jsxs("div", { ref: menu.panel, className: css.panel, style: menu.style, children: [error && _jsx("div", { role: "alert", children: error }), !directory && _jsx("span", { role: "status", children: t('loading') }), _jsx(ModelConfigurationFields, { directory: directory, value: { model: value ? { mode: 'value', value } : { mode: 'default' }, effort: effort !== undefined && effort.value !== '' ? { mode: 'value', value: effort.value } : { mode: 'default' } }, onChange: selection => {
|
|
52
|
+
onChange(selection.model.mode === 'value' ? selection.model.value : '');
|
|
53
|
+
effort?.onChange(selection.effort.mode === 'value' ? selection.effort.value : '');
|
|
54
|
+
// The effort-capable menu navigates between panes instead of
|
|
55
|
+
// closing on a pick (the MemberConfiguration card's behavior).
|
|
56
|
+
if (effort === undefined)
|
|
57
|
+
setOpen(false);
|
|
58
|
+
}, showEffort: effort !== undefined, disabled: disabled, t: t })] })] });
|
|
59
|
+
}
|
|
60
|
+
/** The form-field face: a model select and, when asked, the model's effort select ('' = the model's default). */
|
|
61
|
+
function ModelSelects({ directory, error, value, onChange, effort, disabled, classes, t }) {
|
|
62
|
+
const entries = directory?.entries ?? [];
|
|
63
|
+
// The chosen model stays listed even when the directory hides it.
|
|
64
|
+
const shown = entries.filter(entry => !entry.hidden || entry.value === value);
|
|
65
|
+
const reasoning = entries.find(entry => entry.value === value)?.reasoning;
|
|
66
|
+
const options = reasoning?.options ?? [];
|
|
67
|
+
const defaultEffort = options.find(option => option.value === reasoning?.default)?.label ?? reasoning?.default;
|
|
68
|
+
const pickModel = (next) => {
|
|
69
|
+
onChange(next);
|
|
70
|
+
// An effort the next model does not offer falls back to its default.
|
|
71
|
+
const nextOptions = entries.find(entry => entry.value === next)?.reasoning?.options ?? [];
|
|
72
|
+
if (effort !== undefined && effort.value !== '' && !nextOptions.some(option => option.value === effort.value))
|
|
73
|
+
effort.onChange('');
|
|
74
|
+
};
|
|
75
|
+
return _jsxs("div", { className: classes.row, children: [_jsxs("label", { className: classes.field, children: [_jsx("span", { className: classes.label, children: t('configuration.model') }), _jsxs("select", { className: classes.select, "aria-label": t('configuration.model'), value: value, disabled: disabled || directory === undefined, onChange: event => { pickModel(event.target.value); }, children: [directory === undefined && _jsx("option", { value: value, children: value || t('loading') }), directory !== undefined && value === '' && _jsx("option", { value: "", children: t('configuration.choose') }), directory !== undefined && value !== '' && !shown.some(entry => entry.value === value) && _jsx("option", { value: value, children: value }), shown.map(entry => _jsx("option", { value: entry.value, children: entry.label }, entry.value))] }), error !== undefined && _jsx("span", { role: "alert", children: error })] }), effort !== undefined && _jsxs("label", { className: classes.field, children: [_jsx("span", { className: classes.label, children: t('configuration.effort') }), _jsxs("select", { className: classes.select, "aria-label": t('configuration.effort'), value: effort.value, disabled: disabled || options.length === 0, onChange: event => { effort.onChange(event.target.value); }, children: [_jsx("option", { value: "", children: defaultEffort === undefined ? t('configuration.effortDefault') : `${t('configuration.effortDefault')}(${defaultEffort})` }), options.map(option => _jsx("option", { value: option.value, children: option.label }, option.value))] })] })] });
|
|
48
76
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** Generation-time text rendered through the public Markdown component. */
|
|
3
|
-
import { useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';
|
|
4
|
-
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
4
|
+
import { DisclosureRow, MarkdownText, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
5
|
+
import { IconThinkOutlineRegular } from './icons.js';
|
|
6
|
+
import css from './MemberLiveOutput.module.css';
|
|
5
7
|
export function MemberLiveNode({ node, outputs, t }) {
|
|
6
8
|
const data = node.data;
|
|
7
9
|
const store = outputs.get(data.sessionId);
|
|
@@ -20,6 +22,35 @@ export function MemberLiveOutputView({ sessionId, startedAt, outputs, t }) {
|
|
|
20
22
|
* The wall-clock subtraction is meaningful only when browser and host share a clock.
|
|
21
23
|
* Keep independent callbacks across updates so slow paints cannot disappear from P95.
|
|
22
24
|
*/
|
|
25
|
+
/** The latest completed paragraph's first line — the host think row's running summary. */
|
|
26
|
+
function latestParagraphLine(text) {
|
|
27
|
+
let summary = '';
|
|
28
|
+
let start = 0;
|
|
29
|
+
const separator = /\r?\n(?:[\t ]*\r?\n)+/g;
|
|
30
|
+
while (true) {
|
|
31
|
+
const next = separator.exec(text);
|
|
32
|
+
const end = next === null ? text.length : next.index + next[0].indexOf('\n');
|
|
33
|
+
const newline = text.indexOf('\n', start);
|
|
34
|
+
if (newline !== -1 && newline <= end) {
|
|
35
|
+
const candidate = text.slice(start, newline).trim();
|
|
36
|
+
if (candidate !== '')
|
|
37
|
+
summary = candidate;
|
|
38
|
+
}
|
|
39
|
+
if (next === null)
|
|
40
|
+
return summary;
|
|
41
|
+
start = next.index + next[0].length;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A streaming think block drawn as the host's own think row: collapsed to one
|
|
46
|
+
* line (icon, title, the latest paragraph's first line shimmering), the full
|
|
47
|
+
* text on expand — the same row the settled message shows once it lands.
|
|
48
|
+
*/
|
|
49
|
+
function LiveReasoning({ text, labels, title }) {
|
|
50
|
+
const [open, setOpen] = useState(false);
|
|
51
|
+
const summary = useMemo(() => (latestParagraphLine(text) || text.split('\n', 1)[0] || '').replaceAll('**', ''), [text]);
|
|
52
|
+
return (_jsx(DisclosureRow, { rowClassName: css.thinkRow, leadingClassName: css.leading, titleClassName: css.title, icon: _jsx(IconThinkOutlineRegular, { size: 14 }), title: title, running: true, open: open, expandable: true, expandOnRowClick: true, onToggle: () => { setOpen(value => !value); }, collapsedContent: open || summary === '' ? undefined : _jsxs(_Fragment, { children: [_jsx("span", { className: css.separator, "aria-hidden": true }), _jsx("span", { className: css.summary, children: _jsx(TextShimmer, { children: summary }) })] }), children: open ? _jsx("div", { className: css.thinkBody, children: _jsx(MarkdownText, { text: text, labels: labels }) }) : undefined }));
|
|
53
|
+
}
|
|
23
54
|
function StreamText({ id, kind, text, receivedAt, recovering, sessionId, turn, revision, baseline, surface, diagnostics, t }) {
|
|
24
55
|
const element = useRef(null);
|
|
25
56
|
const samples = useRef([]);
|
|
@@ -70,5 +101,8 @@ function StreamText({ id, kind, text, receivedAt, recovering, sessionId, turn, r
|
|
|
70
101
|
});
|
|
71
102
|
frames.current.add(first);
|
|
72
103
|
}, [receivedAt, revision, baseline, diagnostics, sessionId, turn, surface]);
|
|
73
|
-
|
|
104
|
+
const labels = { code: { copyLabel: t('stream.copy'), copiedLabel: t('stream.copied') }, footnotes: t('stream.footnotes') };
|
|
105
|
+
return _jsxs("section", { ref: element, className: css.item, "data-member-live": id, "data-live-received-at": receivedAt, children: [kind === 'think'
|
|
106
|
+
? _jsx(LiveReasoning, { text: text, labels: labels, title: t('stream.thinking') })
|
|
107
|
+
: _jsx("div", { className: css.text, children: _jsx(MarkdownText, { text: text, labels: labels }) }), recovering && _jsx("span", { className: css.recovering, children: t('stream.recovering') })] });
|
|
74
108
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
|
|
3
|
-
* (v0.1
|
|
3
|
+
* (v0.2.1-rc.1, harness 1600d8a1e1) — do not edit by hand. The 0.1.5 and rc.1 host lines
|
|
4
4
|
* share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
|
|
5
5
|
* no host runtime identity, identical on every line. Re-run the generator
|
|
6
6
|
* after adding an icon import or bumping the harness checkout.
|
|
@@ -18,5 +18,6 @@ export declare const IconCheckOutlineMedium: ({ size, className }: IconProps) =>
|
|
|
18
18
|
export declare const IconChevronDownOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
19
19
|
export declare const IconChevronUpOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
20
20
|
export declare const IconQueueOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
21
|
+
export declare const IconThinkOutlineRegular: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
21
22
|
export declare const IconTrashOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
22
23
|
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -3,4 +3,5 @@ export const IconCheckOutlineMedium = ({ size = 16, className }) => (_jsx("svg",
|
|
|
3
3
|
export const IconChevronDownOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6", stroke: "currentColor" }) }));
|
|
4
4
|
export const IconChevronUpOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M12 10L8.70711 6.70711C8.31658 6.31658 7.68342 6.31658 7.29289 6.70711L4 10", stroke: "currentColor" }) }));
|
|
5
5
|
export const IconQueueOutlineMedium = ({ size = 14, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M5 6.75H11", stroke: "currentColor" }), _jsx("path", { d: "M5 9H8", stroke: "currentColor" }), _jsx("path", { d: "M2.37067 11.2497C1.5872 9.89252 1.32042 8.29798 1.61945 6.7597C1.91847 5.22141 2.76317 3.84293 3.99801 2.87809C5.23285 1.91325 6.7747 1.427 8.33964 1.50888C9.90458 1.59076 11.3873 2.23526 12.5147 3.32369C13.6422 4.41232 14.3384 5.8717 14.4751 7.43304C14.6118 8.99438 14.1797 10.5525 13.2585 11.8205C12.3372 13.0885 10.9889 13.9809 9.4617 14.3334C8.18666 14.6277 6.8587 14.529 5.64964 14.0601C5.17095 13.8745 4.76937 13.4929 4.26509 13.3963C3.67389 13.2832 2.95232 13.5595 2.0377 14.3334", stroke: "currentColor" })] }));
|
|
6
|
+
export const IconThinkOutlineRegular = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1, children: [_jsx("path", { d: "M10.2854 5.71481C12.9673 8.39663 14.1182 11.5938 12.8562 12.8559C11.5942 14.1179 8.39706 12.9669 5.71518 10.2851C3.03333 7.60323 1.88236 4.40608 3.14441 3.14403C4.40644 1.882 7.6036 3.03297 10.2854 5.71481Z", stroke: "currentColor" }), _jsx("path", { d: "M10.2854 10.2851C7.6036 12.9669 4.40644 14.1179 3.14441 12.8559C1.88236 11.5938 3.03333 8.39663 5.71518 5.71481C8.39706 3.03297 11.5942 1.882 12.8562 3.14403C14.1182 4.40608 12.9673 7.60323 10.2854 10.2851Z", stroke: "currentColor" }), _jsx("path", { d: "M8.86291 8.0002C8.86291 8.47549 8.47762 8.86087 8.00224 8.86087C7.52694 8.86087 7.1416 8.47549 7.1416 8.0002C7.1416 7.52485 7.52694 7.13953 8.00224 7.13953C8.47762 7.13953 8.86291 7.52485 8.86291 8.0002Z", fill: "currentColor" })] }));
|
|
6
7
|
export const IconTrashOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M1.28149 3.88831H14.7187", stroke: "currentColor" }), _jsx("path", { d: "M5.41602 3.88833V2.47962C5.41602 2.29282 5.52492 2.11366 5.71876 1.98157C5.9126 1.84948 6.17551 1.77527 6.44964 1.77527H9.55053C9.82466 1.77527 10.0876 1.84948 10.2814 1.98157C10.4753 2.11366 10.5842 2.29282 10.5842 2.47962V3.88833", stroke: "currentColor" }), _jsx("path", { d: "M2.57349 3.88831L3.19366 13.2943C3.21937 13.5502 3.33952 13.7872 3.53065 13.9593C3.72178 14.1313 3.97016 14.2259 4.22729 14.2246H11.7728C12.0299 14.2259 12.2783 14.1313 12.4694 13.9593C12.6605 13.7872 12.7807 13.5502 12.8064 13.2943L13.4266 3.88831", stroke: "currentColor" }), _jsx("path", { d: "M6.44946 6.98926V11.1238", stroke: "currentColor" }), _jsx("path", { d: "M9.55054 6.98926V11.1238", stroke: "currentColor" })] }));
|
|
@@ -23,6 +23,7 @@ import { MemberFeeds } from './member-feed.js';
|
|
|
23
23
|
*/
|
|
24
24
|
import { createElement } from 'react';
|
|
25
25
|
import { HarnessModelPicker } from './HarnessModelPicker.js';
|
|
26
|
+
import { scopeDirectoryToList } from './model-directory-scope.js';
|
|
26
27
|
import { LocalAgentFamilyCard } from './FamilyCard.js';
|
|
27
28
|
import { HarnessCardRegistry } from './harness-cards.js';
|
|
28
29
|
import { MemberConfiguration } from './MemberConfiguration.js';
|
|
@@ -87,8 +88,21 @@ export async function apply(ctx) {
|
|
|
87
88
|
let face = directoryFaces.get(name);
|
|
88
89
|
if (face === undefined)
|
|
89
90
|
directoryFaces.set(name, face = {
|
|
90
|
-
read: async (refresh) =>
|
|
91
|
-
|
|
91
|
+
read: async (refresh) => {
|
|
92
|
+
const directory = unwrap(await gateway.modelDirectory(name, undefined, refresh));
|
|
93
|
+
return directory === null ? null : scopeDirectoryToList(directory, unwrap(await gateway.modelList(name)));
|
|
94
|
+
},
|
|
95
|
+
follow: async function* (signal) {
|
|
96
|
+
// The invite picker faces the curated list, not the raw served
|
|
97
|
+
// catalog; re-read the list on each directory revision.
|
|
98
|
+
let list = unwrap(await gateway.modelList(name));
|
|
99
|
+
for await (const directory of gateway.followModelDirectory(name, undefined, signal)) {
|
|
100
|
+
if (signal.aborted)
|
|
101
|
+
return;
|
|
102
|
+
list = unwrap(await gateway.modelList(name));
|
|
103
|
+
yield scopeDirectoryToList(directory, list);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
92
106
|
});
|
|
93
107
|
return face;
|
|
94
108
|
};
|
|
@@ -24,6 +24,7 @@ export declare const zh: {
|
|
|
24
24
|
readonly "configuration.details": "模型目录详情";
|
|
25
25
|
readonly "configuration.model": "模型";
|
|
26
26
|
readonly "configuration.effort": "推理强度";
|
|
27
|
+
readonly "configuration.effortDefault": "默认";
|
|
27
28
|
readonly "configuration.inherit": "继承成员创建配置";
|
|
28
29
|
readonly "configuration.default": "跟随 harness 默认值";
|
|
29
30
|
readonly "configuration.search": "搜索模型名称或 ID";
|
|
@@ -55,7 +56,6 @@ export declare const zh: {
|
|
|
55
56
|
readonly "configuration.cancelled": "已接收撤销请求,正在恢复配置。";
|
|
56
57
|
readonly "configuration.failed": "配置操作失败,请检查原因后重试。";
|
|
57
58
|
readonly 'stream.thinking': '思考中';
|
|
58
|
-
readonly 'stream.writing': '正在生成';
|
|
59
59
|
readonly 'stream.recovering': '部分记录,等待实时连接';
|
|
60
60
|
readonly 'stream.copy': '复制';
|
|
61
61
|
readonly 'stream.copied': '已复制';
|
|
@@ -24,6 +24,7 @@ export const zh = {
|
|
|
24
24
|
"configuration.details": "模型目录详情",
|
|
25
25
|
"configuration.model": "模型",
|
|
26
26
|
"configuration.effort": "推理强度",
|
|
27
|
+
"configuration.effortDefault": "默认",
|
|
27
28
|
"configuration.inherit": "继承成员创建配置",
|
|
28
29
|
"configuration.default": "跟随 harness 默认值",
|
|
29
30
|
"configuration.search": "搜索模型名称或 ID",
|
|
@@ -55,7 +56,6 @@ export const zh = {
|
|
|
55
56
|
"configuration.cancelled": "已接收撤销请求,正在恢复配置。",
|
|
56
57
|
"configuration.failed": "配置操作失败,请检查原因后重试。",
|
|
57
58
|
'stream.thinking': '思考中',
|
|
58
|
-
'stream.writing': '正在生成',
|
|
59
59
|
'stream.recovering': '部分记录,等待实时连接',
|
|
60
60
|
'stream.copy': '复制',
|
|
61
61
|
'stream.copied': '已复制',
|
|
@@ -160,6 +160,7 @@ export const en = {
|
|
|
160
160
|
"configuration.details": "Model directory details",
|
|
161
161
|
"configuration.model": "Model",
|
|
162
162
|
"configuration.effort": "Reasoning effort",
|
|
163
|
+
"configuration.effortDefault": "Default",
|
|
163
164
|
"configuration.inherit": "Inherit member configuration",
|
|
164
165
|
"configuration.default": "Follow harness defaults",
|
|
165
166
|
"configuration.search": "Search model names or IDs",
|
|
@@ -191,7 +192,6 @@ export const en = {
|
|
|
191
192
|
"configuration.cancelled": "Cancellation accepted; restoring configuration.",
|
|
192
193
|
"configuration.failed": "Configuration failed; check the reason before retrying.",
|
|
193
194
|
'stream.thinking': 'Thinking',
|
|
194
|
-
'stream.writing': 'Writing',
|
|
195
195
|
'stream.recovering': 'Partial record, awaiting live connection',
|
|
196
196
|
'stream.copy': 'Copy',
|
|
197
197
|
'stream.copied': 'Copied',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LocalAgentModelDirectory } from '../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Scope a harness's served directory to its curated model list (models.json):
|
|
4
|
+
* the list IS the invite picker's vocabulary, in the list's own order — a
|
|
5
|
+
* listed value the endpoint no longer serves stays as a bare row (the member
|
|
6
|
+
* dialog shows it the same way). No list support, or none configured, keeps
|
|
7
|
+
* the directory as served. Mirrors the member dialog's scoping rule so the
|
|
8
|
+
* invite picker and the member configuration show the same candidates.
|
|
9
|
+
* @param directory - the harness's served directory.
|
|
10
|
+
* @param list - the curated shortlist values, or null when unsupported.
|
|
11
|
+
* @returns the scoped directory.
|
|
12
|
+
*/
|
|
13
|
+
export declare function scopeDirectoryToList(directory: LocalAgentModelDirectory, list: readonly string[] | null | undefined): LocalAgentModelDirectory;
|
|
14
|
+
//# sourceMappingURL=model-directory-scope.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope a harness's served directory to its curated model list (models.json):
|
|
3
|
+
* the list IS the invite picker's vocabulary, in the list's own order — a
|
|
4
|
+
* listed value the endpoint no longer serves stays as a bare row (the member
|
|
5
|
+
* dialog shows it the same way). No list support, or none configured, keeps
|
|
6
|
+
* the directory as served. Mirrors the member dialog's scoping rule so the
|
|
7
|
+
* invite picker and the member configuration show the same candidates.
|
|
8
|
+
* @param directory - the harness's served directory.
|
|
9
|
+
* @param list - the curated shortlist values, or null when unsupported.
|
|
10
|
+
* @returns the scoped directory.
|
|
11
|
+
*/
|
|
12
|
+
export function scopeDirectoryToList(directory, list) {
|
|
13
|
+
if (list === undefined || list === null || list.length === 0)
|
|
14
|
+
return directory;
|
|
15
|
+
const byValue = new Map(directory.entries.flatMap(entry => entry.resolvedModel === undefined
|
|
16
|
+
? [[entry.value, entry]]
|
|
17
|
+
: [[entry.value, entry], [entry.resolvedModel, entry]]));
|
|
18
|
+
const entries = list.map(value => byValue.get(value) ?? { value, label: value, source: 'configuration' });
|
|
19
|
+
return { ...directory, entries };
|
|
20
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1190,8 +1190,12 @@ export declare class LocalAgentRegistry {
|
|
|
1190
1190
|
* const handle = await ctx.sessionPersistence.open(SessionId(childSessionId), 'write')
|
|
1191
1191
|
* try {
|
|
1192
1192
|
* const cold = await handle.read(0)
|
|
1193
|
+
* // Close a turn a restart interrupted, or the next round's turn/start
|
|
1194
|
+
* // makes the stored log unreadable.
|
|
1195
|
+
* const closers = interruptedTurnClosers(cold.events)
|
|
1196
|
+
* if (closers.length > 0) await handle.append(closers)
|
|
1193
1197
|
* const session = ctx.sessions.prepare(SessionId(childSessionId), {
|
|
1194
|
-
* seed: [...cold.events],
|
|
1198
|
+
* seed: [...cold.events, ...closers],
|
|
1195
1199
|
* meta: structuredClone(handle.header),
|
|
1196
1200
|
* inheritedEventCount: handle.inheritedEventCount,
|
|
1197
1201
|
* eventState: cold.eventState,
|
|
@@ -1199,7 +1203,7 @@ export declare class LocalAgentRegistry {
|
|
|
1199
1203
|
* // The seeded constructor's session/end-seed marker lands before enter()
|
|
1200
1204
|
* // installs publication hooks — push the unstored suffix through the
|
|
1201
1205
|
* // handle directly, or the writer's contiguous cursor diverges.
|
|
1202
|
-
* const unstored = session.snapshotEvents().slice(cold.events.length)
|
|
1206
|
+
* const unstored = session.snapshotEvents().slice(cold.events.length + closers.length)
|
|
1203
1207
|
* if (unstored.length > 0) await handle.append(unstored)
|
|
1204
1208
|
* const detach = ctx.sessions.enter(session)
|
|
1205
1209
|
* try { ctx.sessions.announce(session) }
|
|
@@ -1336,6 +1340,18 @@ export declare class LocalAgentRegistry {
|
|
|
1336
1340
|
* @param session - the live child session to sync.
|
|
1337
1341
|
*/
|
|
1338
1342
|
syncChildSession(session: Session, strict?: boolean): Promise<void>;
|
|
1343
|
+
/**
|
|
1344
|
+
* One sync pass. While the write handle is open the host routes every live
|
|
1345
|
+
* append into the writer's buffer, so the routed events are drained first
|
|
1346
|
+
* (`sessions.flush`: drain + flush) and only what is STILL missing — events
|
|
1347
|
+
* from before the handle opened, or a host that routes nothing — goes in
|
|
1348
|
+
* explicitly, bounded to the events that existed before the drain. Appending
|
|
1349
|
+
* routed-but-undrained events explicitly as well would land them twice: the
|
|
1350
|
+
* explicit write wins, the buffered copy then fails contiguity forever, and
|
|
1351
|
+
* every later flush — the host's tool-execution checkpoint included —
|
|
1352
|
+
* throws "append seq mismatch".
|
|
1353
|
+
*/
|
|
1354
|
+
private syncChildSessionOnce;
|
|
1339
1355
|
/**
|
|
1340
1356
|
* Get or open the child session's registry-owned WRITE handle (the
|
|
1341
1357
|
* {@link childWriteHandles} cache). When the caller carries the session
|
package/lib/types/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { appendFileSync, chmodSync, mkdirSync, readFileSync, statSync } from 'no
|
|
|
30
30
|
import { join } from 'node:path';
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import z from '@deepseek-ai/schemastery';
|
|
33
|
+
import * as sessionModule from '@deepseek-ai/dsh-session';
|
|
33
34
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
34
35
|
// The durable reader validates plugin vocabulary before replay. Register in
|
|
35
36
|
// both resolution planes: source-based host toolchains and installed bundles
|
|
@@ -46,6 +47,7 @@ for (const catalog of streamCatalogs) {
|
|
|
46
47
|
}
|
|
47
48
|
import LocalAgentGateway from './gateway.js';
|
|
48
49
|
import { MemberChannel } from './member-channel.js';
|
|
50
|
+
import { PresetToolHost } from './preset-tool-host.js';
|
|
49
51
|
import { MemberInbox } from './member-inbox.js';
|
|
50
52
|
import { PreparedMembers } from './prepared-members.js';
|
|
51
53
|
import { MemberControls } from './member-controls.js';
|
|
@@ -540,6 +542,11 @@ export function parseScopeFlag(input) {
|
|
|
540
542
|
}
|
|
541
543
|
return { input: rest.join(' ').trim(), ...scope === undefined ? {} : { scope } };
|
|
542
544
|
}
|
|
545
|
+
/** A delegation record without its requested model. */
|
|
546
|
+
function withoutModel(record) {
|
|
547
|
+
const { model: _model, ...rest } = record;
|
|
548
|
+
return rest;
|
|
549
|
+
}
|
|
543
550
|
/** Map a login failure to a command error, naming the swallowed signal. */
|
|
544
551
|
function loginFailure(harness, exitCode, signal) {
|
|
545
552
|
return `${harness.name} login exited with ${signal ?? `code ${exitCode ?? 'unknown'}`}; try /${harness.name} login again.`;
|
|
@@ -1171,7 +1178,12 @@ export class LocalAgentRegistry {
|
|
|
1171
1178
|
const binding = this.memberControls.binding(record.childSessionId);
|
|
1172
1179
|
if (binding !== undefined)
|
|
1173
1180
|
record = {
|
|
1174
|
-
|
|
1181
|
+
// The model a resume re-requests is the bound one, verbatim: a provider
|
|
1182
|
+
// recording its resolved model ('opus' → 'claude-opus-5-5', or the
|
|
1183
|
+
// default a model-less start resolved to) would make the next round's
|
|
1184
|
+
// binding differ and the member refuse it ("identity changed: model").
|
|
1185
|
+
...withoutModel(record),
|
|
1186
|
+
...binding.model === undefined ? {} : { model: binding.model },
|
|
1175
1187
|
...binding.effort === undefined ? {} : { effort: binding.effort },
|
|
1176
1188
|
...binding.configurationLock === undefined ? {} : { configurationLock: binding.configurationLock },
|
|
1177
1189
|
};
|
|
@@ -1720,8 +1732,12 @@ export class LocalAgentRegistry {
|
|
|
1720
1732
|
* const handle = await ctx.sessionPersistence.open(SessionId(childSessionId), 'write')
|
|
1721
1733
|
* try {
|
|
1722
1734
|
* const cold = await handle.read(0)
|
|
1735
|
+
* // Close a turn a restart interrupted, or the next round's turn/start
|
|
1736
|
+
* // makes the stored log unreadable.
|
|
1737
|
+
* const closers = interruptedTurnClosers(cold.events)
|
|
1738
|
+
* if (closers.length > 0) await handle.append(closers)
|
|
1723
1739
|
* const session = ctx.sessions.prepare(SessionId(childSessionId), {
|
|
1724
|
-
* seed: [...cold.events],
|
|
1740
|
+
* seed: [...cold.events, ...closers],
|
|
1725
1741
|
* meta: structuredClone(handle.header),
|
|
1726
1742
|
* inheritedEventCount: handle.inheritedEventCount,
|
|
1727
1743
|
* eventState: cold.eventState,
|
|
@@ -1729,7 +1745,7 @@ export class LocalAgentRegistry {
|
|
|
1729
1745
|
* // The seeded constructor's session/end-seed marker lands before enter()
|
|
1730
1746
|
* // installs publication hooks — push the unstored suffix through the
|
|
1731
1747
|
* // handle directly, or the writer's contiguous cursor diverges.
|
|
1732
|
-
* const unstored = session.snapshotEvents().slice(cold.events.length)
|
|
1748
|
+
* const unstored = session.snapshotEvents().slice(cold.events.length + closers.length)
|
|
1733
1749
|
* if (unstored.length > 0) await handle.append(unstored)
|
|
1734
1750
|
* const detach = ctx.sessions.enter(session)
|
|
1735
1751
|
* try { ctx.sessions.announce(session) }
|
|
@@ -2019,8 +2035,18 @@ export class LocalAgentRegistry {
|
|
|
2019
2035
|
const handle = await this.acquireChildWriteHandle(childSessionId);
|
|
2020
2036
|
try {
|
|
2021
2037
|
const cold = await handle.read(0);
|
|
2038
|
+
// A host restart mid-round leaves the child's last turn open on disk;
|
|
2039
|
+
// the next round's turn/start would then follow an unclosed turn and
|
|
2040
|
+
// the strict reader rejects the whole log. Close it the way the agent
|
|
2041
|
+
// loop's own resume does: synthetic closers (missing tool errors,
|
|
2042
|
+
// step/end, an `interrupted` turn/end) appended through the handle.
|
|
2043
|
+
// Older hosts without the helper keep the previous behavior.
|
|
2044
|
+
const closeOpenTurn = sessionModule.interruptedTurnClosers;
|
|
2045
|
+
const closers = closeOpenTurn === undefined ? [] : closeOpenTurn(cold.events);
|
|
2046
|
+
if (closers.length > 0)
|
|
2047
|
+
await handle.append(closers);
|
|
2022
2048
|
const session = sessions.prepare(SessionId(childSessionId), {
|
|
2023
|
-
seed: [...cold.events],
|
|
2049
|
+
seed: [...cold.events, ...closers],
|
|
2024
2050
|
meta: structuredClone(handle.header),
|
|
2025
2051
|
inheritedEventCount: handle.inheritedEventCount,
|
|
2026
2052
|
eventState: cold.eventState,
|
|
@@ -2030,7 +2056,7 @@ export class LocalAgentRegistry {
|
|
|
2030
2056
|
// — push the unstored suffix through the handle directly (the agent
|
|
2031
2057
|
// loop's resume does the same via appendUnstoredSuffix), or the writer's
|
|
2032
2058
|
// contiguous cursor and the session log diverge.
|
|
2033
|
-
const unstored = session.snapshotEvents().slice(cold.events.length);
|
|
2059
|
+
const unstored = session.snapshotEvents().slice(cold.events.length + closers.length);
|
|
2034
2060
|
if (unstored.length > 0)
|
|
2035
2061
|
await handle.append(unstored);
|
|
2036
2062
|
const detach = sessions.enter(session);
|
|
@@ -2065,20 +2091,48 @@ export class LocalAgentRegistry {
|
|
|
2065
2091
|
* @param session - the live child session to sync.
|
|
2066
2092
|
*/
|
|
2067
2093
|
async syncChildSession(session, strict = false) {
|
|
2094
|
+
const id = String(session.id);
|
|
2068
2095
|
try {
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2096
|
+
try {
|
|
2097
|
+
await this.syncChildSessionOnce(session);
|
|
2098
|
+
}
|
|
2099
|
+
catch (error) {
|
|
2100
|
+
if (!/append seq mismatch/.test(error instanceof Error ? error.message : String(error)))
|
|
2101
|
+
throw error;
|
|
2102
|
+
// A routed live buffer the explicit suffix raced (see
|
|
2103
|
+
// syncChildSessionOnce) stays poisoned for the handle's lifetime:
|
|
2104
|
+
// drop the handle with it and catch up from disk on a fresh one.
|
|
2105
|
+
await this.releaseChildWriteHandle(id);
|
|
2106
|
+
await this.syncChildSessionOnce(session);
|
|
2107
|
+
}
|
|
2075
2108
|
}
|
|
2076
2109
|
catch (error) {
|
|
2077
2110
|
if (strict)
|
|
2078
2111
|
throw error;
|
|
2079
|
-
this.ctx.logger.warn(`localAgent: syncing child session ${
|
|
2112
|
+
this.ctx.logger.warn(`localAgent: syncing child session ${id} to persistence failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2080
2113
|
}
|
|
2081
2114
|
}
|
|
2115
|
+
/**
|
|
2116
|
+
* One sync pass. While the write handle is open the host routes every live
|
|
2117
|
+
* append into the writer's buffer, so the routed events are drained first
|
|
2118
|
+
* (`sessions.flush`: drain + flush) and only what is STILL missing — events
|
|
2119
|
+
* from before the handle opened, or a host that routes nothing — goes in
|
|
2120
|
+
* explicitly, bounded to the events that existed before the drain. Appending
|
|
2121
|
+
* routed-but-undrained events explicitly as well would land them twice: the
|
|
2122
|
+
* explicit write wins, the buffered copy then fails contiguity forever, and
|
|
2123
|
+
* every later flush — the host's tool-execution checkpoint included —
|
|
2124
|
+
* throws "append seq mismatch".
|
|
2125
|
+
*/
|
|
2126
|
+
async syncChildSessionOnce(session) {
|
|
2127
|
+
const handle = await this.acquireChildWriteHandle(String(session.id), session.header);
|
|
2128
|
+
const known = session.snapshotEvents().length;
|
|
2129
|
+
await this.ctx.get('sessions')?.flush(session);
|
|
2130
|
+
const stored = await handle.read(0);
|
|
2131
|
+
const missing = session.snapshotEvents().slice(stored.events.length, known);
|
|
2132
|
+
if (missing.length > 0)
|
|
2133
|
+
await handle.append(missing);
|
|
2134
|
+
await handle.flush();
|
|
2135
|
+
}
|
|
2082
2136
|
/**
|
|
2083
2137
|
* Get or open the child session's registry-owned WRITE handle (the
|
|
2084
2138
|
* {@link childWriteHandles} cache). When the caller carries the session
|
|
@@ -2735,9 +2789,22 @@ export async function apply(ctx, config) {
|
|
|
2735
2789
|
// The member channel's loopback listener (bridge MCP servers call back here).
|
|
2736
2790
|
// Degrade, don't explode: a failed bind leaves member_message failing with a
|
|
2737
2791
|
// tool error while everything else works.
|
|
2738
|
-
|
|
2792
|
+
// Mode tools: a member with a mode reaches that preset's plugin tools
|
|
2793
|
+
// through the same bridge (see preset-tool-host.ts).
|
|
2794
|
+
const modeTools = new PresetToolHost(ctx, join(config.homesRoot, 'mode-tools.jsonl'));
|
|
2795
|
+
const memberChannel = new MemberChannel(ctx, registry, registry.memberBridgeSocketPath(), modeTools);
|
|
2739
2796
|
void memberChannel.start();
|
|
2740
|
-
ctx.
|
|
2797
|
+
ctx.on('approval/request', async (request, next) => {
|
|
2798
|
+
const child = modeTools.memberOf(request.agent);
|
|
2799
|
+
const delegation = child === undefined ? undefined : registry.getDelegation(child);
|
|
2800
|
+
const room = ctx.get.call(ctx, 'room');
|
|
2801
|
+
if (child === undefined || delegation === undefined || room?.askMemberApproval === undefined)
|
|
2802
|
+
return next();
|
|
2803
|
+
const reason = request.displayReason ?? request.reason;
|
|
2804
|
+
const outcome = await room.askMemberApproval({ childSessionId: child, parentSessionId: delegation.parentSessionId, provider: delegation.provider }, { toolName: request.toolName, ...reason === undefined ? {} : { reason }, ...request.signal === undefined ? {} : { signal: request.signal } });
|
|
2805
|
+
return outcome === 'unavailable' ? next() : outcome;
|
|
2806
|
+
}, { prepend: true });
|
|
2807
|
+
ctx.effect(() => () => { void memberChannel.dispose(); void modeTools.dispose(); });
|
|
2741
2808
|
ctx.commands.register({
|
|
2742
2809
|
name: 'local-agent',
|
|
2743
2810
|
description: 'list the registered local code-agent harnesses, or stop an in-flight delegation',
|
|
@@ -27,7 +27,18 @@ export declare class LiveStreamPublisher {
|
|
|
27
27
|
private readonly persist;
|
|
28
28
|
private readonly onError;
|
|
29
29
|
private readonly states;
|
|
30
|
+
/** Items shown live but not yet in the log: their step cannot open before the earlier steps close. */
|
|
31
|
+
private readonly previews;
|
|
30
32
|
constructor(streams: LocalAgentStreams, session: Session, turn: number, persist: () => void, onError: (error: unknown) => void);
|
|
33
|
+
/**
|
|
34
|
+
* Show an item live without writing it to the session: a step may open in
|
|
35
|
+
* the log only after every earlier step has closed, so a later item that
|
|
36
|
+
* streams first waits here (browser-only) until the driver can open it.
|
|
37
|
+
*/
|
|
38
|
+
preview(step: number, kind: 'think' | 'text', text: string, options?: {
|
|
39
|
+
itemId?: string;
|
|
40
|
+
receivedAt?: number;
|
|
41
|
+
}): void;
|
|
31
42
|
update(step: number, kind: 'think' | 'text', text: string, options?: {
|
|
32
43
|
itemId?: string;
|
|
33
44
|
receivedAt?: number;
|
package/lib/types/live-stream.js
CHANGED
|
@@ -97,6 +97,8 @@ export class LiveStreamPublisher {
|
|
|
97
97
|
persist;
|
|
98
98
|
onError;
|
|
99
99
|
states = new Map();
|
|
100
|
+
/** Items shown live but not yet in the log: their step cannot open before the earlier steps close. */
|
|
101
|
+
previews = new Map();
|
|
100
102
|
constructor(streams, session, turn, persist, onError) {
|
|
101
103
|
this.streams = streams;
|
|
102
104
|
this.session = session;
|
|
@@ -104,8 +106,23 @@ export class LiveStreamPublisher {
|
|
|
104
106
|
this.persist = persist;
|
|
105
107
|
this.onError = onError;
|
|
106
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Show an item live without writing it to the session: a step may open in
|
|
111
|
+
* the log only after every earlier step has closed, so a later item that
|
|
112
|
+
* streams first waits here (browser-only) until the driver can open it.
|
|
113
|
+
*/
|
|
114
|
+
preview(step, kind, text, options = {}) {
|
|
115
|
+
const id = `${this.turn}:${step}${options.itemId === undefined ? '' : `:${options.itemId}`}`;
|
|
116
|
+
const previous = this.previews.get(id);
|
|
117
|
+
if (previous?.text === text && previous.kind === kind)
|
|
118
|
+
return;
|
|
119
|
+
const item = { id, turn: this.turn, step, kind, text, receivedAt: previous?.receivedAt ?? options.receivedAt ?? Date.now() };
|
|
120
|
+
this.previews.set(id, item);
|
|
121
|
+
this.streams.publish(String(this.session.id), item);
|
|
122
|
+
}
|
|
107
123
|
update(step, kind, text, options = {}) {
|
|
108
124
|
const id = `${this.turn}:${step}${options.itemId === undefined ? '' : `:${options.itemId}`}`;
|
|
125
|
+
this.previews.delete(id);
|
|
109
126
|
let state = this.states.get(id);
|
|
110
127
|
if (state?.item.text === text && state.item.kind === kind)
|
|
111
128
|
return;
|
|
@@ -150,6 +167,12 @@ export class LiveStreamPublisher {
|
|
|
150
167
|
}
|
|
151
168
|
/** Call only after the corresponding native final message has been appended. */
|
|
152
169
|
finish(step) {
|
|
170
|
+
for (const [id, item] of this.previews) {
|
|
171
|
+
if (item.step !== step)
|
|
172
|
+
continue;
|
|
173
|
+
this.previews.delete(id);
|
|
174
|
+
this.streams.finish(String(this.session.id), id);
|
|
175
|
+
}
|
|
153
176
|
for (const [id, state] of this.states) {
|
|
154
177
|
if (state.item.step !== step)
|
|
155
178
|
continue;
|
|
@@ -167,6 +190,9 @@ export class LiveStreamPublisher {
|
|
|
167
190
|
}
|
|
168
191
|
/** A producer failure keeps a durable partial, without claiming completion. */
|
|
169
192
|
dispose() {
|
|
193
|
+
for (const id of this.previews.keys())
|
|
194
|
+
this.streams.finish(String(this.session.id), id);
|
|
195
|
+
this.previews.clear();
|
|
170
196
|
for (const [id, state] of this.states) {
|
|
171
197
|
this.checkpoint(id);
|
|
172
198
|
if (state.timer !== undefined)
|