@zergai/ztc 0.3.0-beta.1 → 0.4.0-beta.1
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/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/App.js +16 -2
- package/dist/agent/agent.d.ts +20 -8
- package/dist/agent/agent.js +92 -40
- package/dist/agent/backends/gemini.js +1 -1
- package/dist/agent/backends/openai_format.js +2 -2
- package/dist/agent/backends/openai_responses.js +1 -1
- package/dist/agent/backends/types.d.ts +1 -0
- package/dist/agent/commands/config.js +15 -1
- package/dist/agent/commands/settings.js +23 -0
- package/dist/agent/commands/status.js +3 -0
- package/dist/agent/commands/types.d.ts +2 -0
- package/dist/agent/completion_state.js +2 -2
- package/dist/agent/factory.js +7 -1
- package/dist/agent/request_encoder.js +3 -0
- package/dist/agent/runtime/command_jobs.d.ts +47 -0
- package/dist/agent/runtime/command_jobs.js +268 -0
- package/dist/agent/runtime/execution_context.d.ts +16 -0
- package/dist/agent/runtime/execution_context.js +20 -0
- package/dist/agent/runtime/recovery.d.ts +9 -0
- package/dist/agent/runtime/recovery.js +71 -0
- package/dist/agent/runtime/tool_runtime.js +3 -1
- package/dist/agent/runtime/tool_scheduler.d.ts +6 -0
- package/dist/agent/runtime/tool_scheduler.js +57 -0
- package/dist/agent/subagent.js +30 -12
- package/dist/agent/tool_response_store.d.ts +2 -0
- package/dist/agent/tool_response_store.js +1 -0
- package/dist/agent/tools/deferred.js +2 -0
- package/dist/agent/tools/file.js +2 -0
- package/dist/agent/tools/pane.js +139 -42
- package/dist/agent/tools/recall_tool_response.js +1 -0
- package/dist/agent/tools/registry.js +3 -1
- package/dist/agent/tools/search.js +1 -0
- package/dist/agent/tools/shell.d.ts +2 -0
- package/dist/agent/tools/shell.js +57 -2
- package/dist/agent/tools/types.d.ts +4 -0
- package/dist/app/agent_runner.js +1 -1
- package/dist/app/ordered_transcript.d.ts +13 -0
- package/dist/app/ordered_transcript.js +55 -0
- package/dist/cli_ratatui.d.ts +2 -7
- package/dist/cli_ratatui.js +53 -34
- package/dist/config/execution.d.ts +12 -0
- package/dist/config/execution.js +13 -0
- package/dist/config/types.d.ts +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -0
- package/dist/emulation/tool_adapters.js +5 -2
- package/dist/headless.d.ts +1 -18
- package/dist/headless.js +15 -0
- package/dist/plugin/runtime.d.ts +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/ui/ratatui/headless_pane_manager.d.ts +2 -19
- package/dist/ui/ratatui/headless_pane_manager.js +41 -34
- package/dist/ui/ratatui/pane_input.d.ts +4 -0
- package/dist/ui/ratatui/pane_input.js +15 -0
- package/dist/ui/ratatui/pane_relay.d.ts +3 -0
- package/dist/ui/ratatui/pane_relay.js +20 -2
- package/dist/ui/ratatui/protocol.d.ts +17 -0
- package/dist/ui/ratatui/protocol.js +1 -0
- package/dist/utils/shell.d.ts +2 -0
- package/dist/utils/shell.js +54 -11
- package/dist/web/mirror_stream.d.ts +2 -1
- package/dist/web/mirror_stream.js +7 -0
- package/dist/web/pane_registry.js +8 -2
- package/dist/web/view_plugin.d.ts +1 -0
- package/dist/zlm-embed.mjs +384 -118
- package/package.json +2 -2
|
@@ -269,6 +269,7 @@ export class ToolResponseStore {
|
|
|
269
269
|
timestamp: new Date().toISOString(),
|
|
270
270
|
turn: this.turnCounter++,
|
|
271
271
|
partial,
|
|
272
|
+
...(context?.isError === undefined ? {} : { is_error: context.isError }),
|
|
272
273
|
preview,
|
|
273
274
|
};
|
|
274
275
|
const sourcePath = context?.sourcePath ?? null;
|
|
@@ -22,6 +22,8 @@ export const ALWAYS_LOADED_TOOL_NAMES = new Set([
|
|
|
22
22
|
'list_directory',
|
|
23
23
|
'search',
|
|
24
24
|
'run_command',
|
|
25
|
+
'read_command',
|
|
26
|
+
'stop_command',
|
|
25
27
|
'apply_patch',
|
|
26
28
|
// Emulation compatibility aliases. They only appear in the request when
|
|
27
29
|
// the active emulation adds matching tools to the runtime tool list.
|
package/dist/agent/tools/file.js
CHANGED
|
@@ -104,6 +104,7 @@ async function assertTemporaryWriteAllowed(path, context) {
|
|
|
104
104
|
}
|
|
105
105
|
// --- File Tools ---
|
|
106
106
|
export const readFileTool = {
|
|
107
|
+
parallelSafe: true,
|
|
107
108
|
capabilities: [ToolCapability.FILE_READ],
|
|
108
109
|
definition: {
|
|
109
110
|
name: 'read_file',
|
|
@@ -503,6 +504,7 @@ export const editFileTool = {
|
|
|
503
504
|
},
|
|
504
505
|
};
|
|
505
506
|
export const listDirectoryTool = {
|
|
507
|
+
parallelSafe: true,
|
|
506
508
|
capabilities: [ToolCapability.FILE_READ],
|
|
507
509
|
definition: {
|
|
508
510
|
name: 'list_directory',
|
package/dist/agent/tools/pane.js
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* and list terminal panes when running inside zmux.
|
|
6
6
|
* They communicate with the zmux Rust binary via the IPC socket.
|
|
7
7
|
*/
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { ToolCapability } from '../runtime/capabilities.js';
|
|
8
10
|
import { existsSync } from 'fs';
|
|
9
11
|
import { basename, isAbsolute, resolve } from 'path';
|
|
10
12
|
import { isRatatuiBinaryAvailable } from '../../ui/ratatui/process.js';
|
|
13
|
+
import { CAPABILITY_PANE_DELIVERY_ACK } from '../../ui/ratatui/protocol.js';
|
|
11
14
|
import { generateSessionId } from '../../session.js';
|
|
12
15
|
import { isPaneProvider, resolveModelAndProvider } from '../../panes/model_catalog.js';
|
|
13
16
|
import { buildZtcPaneLaunchCommand, createZtcLaunchSpec } from '../../panes/ztc_launch.js';
|
|
@@ -139,19 +142,30 @@ export function clearPaneToolsTuiProcess() {
|
|
|
139
142
|
/**
|
|
140
143
|
* Wait for a specific event type from the TUI process with a timeout.
|
|
141
144
|
*/
|
|
142
|
-
|
|
145
|
+
const PANE_WAIT_CANCELLED = 'Cancelled while waiting for pane response; inspect the pane before retrying because input may already have arrived.';
|
|
146
|
+
function waitForEvent(tui, predicate, timeoutMs = 10000, signal) {
|
|
143
147
|
return new Promise((resolve, reject) => {
|
|
144
|
-
|
|
148
|
+
if (signal?.aborted) {
|
|
149
|
+
reject(new Error(PANE_WAIT_CANCELLED));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
clearTimeout(timeout);
|
|
145
154
|
unsubscribe();
|
|
146
|
-
|
|
155
|
+
signal?.removeEventListener('abort', abort);
|
|
156
|
+
};
|
|
157
|
+
const abort = () => { cleanup(); reject(new Error(PANE_WAIT_CANCELLED)); };
|
|
158
|
+
const timeout = setTimeout(() => {
|
|
159
|
+
cleanup();
|
|
160
|
+
reject(new Error('Timed out waiting for pane response; inspect the pane before retrying.'));
|
|
147
161
|
}, timeoutMs);
|
|
148
162
|
const unsubscribe = tui.on((event) => {
|
|
149
163
|
if (predicate(event)) {
|
|
150
|
-
|
|
151
|
-
unsubscribe();
|
|
164
|
+
cleanup();
|
|
152
165
|
resolve(event);
|
|
153
166
|
}
|
|
154
167
|
});
|
|
168
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
155
169
|
});
|
|
156
170
|
}
|
|
157
171
|
const SPLIT_DIRECTIONS = ['horizontal', 'vertical'];
|
|
@@ -757,68 +771,122 @@ export const createZtcPaneTool = {
|
|
|
757
771
|
}
|
|
758
772
|
},
|
|
759
773
|
};
|
|
774
|
+
// Legacy renderers omit request IDs. Only one tool read per pane may be in
|
|
775
|
+
// flight; viewport events from mirror consumers are filtered separately.
|
|
776
|
+
const legacyPaneReads = new WeakMap();
|
|
777
|
+
async function serializeLegacyPaneRead(transport, paneId, signal, read) {
|
|
778
|
+
let panes = legacyPaneReads.get(transport);
|
|
779
|
+
if (!panes) {
|
|
780
|
+
panes = new Map();
|
|
781
|
+
legacyPaneReads.set(transport, panes);
|
|
782
|
+
}
|
|
783
|
+
const previous = panes.get(paneId) ?? Promise.resolve();
|
|
784
|
+
const result = previous.then(() => {
|
|
785
|
+
if (signal.aborted)
|
|
786
|
+
throw new Error(PANE_WAIT_CANCELLED);
|
|
787
|
+
return read();
|
|
788
|
+
});
|
|
789
|
+
const settled = result.then(() => { }, () => { });
|
|
790
|
+
panes.set(paneId, settled);
|
|
791
|
+
void settled.then(() => { if (panes.get(paneId) === settled)
|
|
792
|
+
panes.delete(paneId); });
|
|
793
|
+
// Cancellation while waiting for a slot is immediate, but does not unlock
|
|
794
|
+
// the preceding request or send a command for this cancelled read.
|
|
795
|
+
let onAbort;
|
|
796
|
+
const aborted = new Promise((_, reject) => {
|
|
797
|
+
onAbort = () => reject(new Error(PANE_WAIT_CANCELLED));
|
|
798
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
799
|
+
if (signal.aborted)
|
|
800
|
+
onAbort();
|
|
801
|
+
});
|
|
802
|
+
try {
|
|
803
|
+
return await Promise.race([result, aborted]);
|
|
804
|
+
}
|
|
805
|
+
finally {
|
|
806
|
+
signal.removeEventListener('abort', onAbort);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
760
809
|
export const readTerminalPaneTool = {
|
|
810
|
+
capabilities: [ToolCapability.FILE_READ],
|
|
761
811
|
definition: {
|
|
762
812
|
name: 'read_terminal_pane',
|
|
763
813
|
description: `Read a pane's text output. Use viewport=true for exactly what is visible, ansi=true when terminal color/control state matters, or lines for recent scrollback.`,
|
|
764
814
|
parameters: {
|
|
765
815
|
type: 'object',
|
|
766
816
|
properties: {
|
|
817
|
+
viewport: { type: 'boolean', description: 'Read visible viewport only.' },
|
|
818
|
+
ansi: { type: 'boolean', description: 'Keep ANSI color/control sequences.' },
|
|
767
819
|
pane_id: {
|
|
768
820
|
type: 'number',
|
|
769
821
|
description: 'The ID of the pane to read (from create_terminal_pane or list_terminal_panes)',
|
|
770
822
|
},
|
|
771
823
|
lines: {
|
|
772
824
|
type: 'number',
|
|
773
|
-
description: 'Number of lines to read from the bottom of the buffer. Defaults to
|
|
825
|
+
description: 'Number of lines to read from the bottom of the buffer. Defaults to 200 lines; maximum 2000.',
|
|
774
826
|
},
|
|
775
827
|
},
|
|
776
828
|
required: ['pane_id'],
|
|
777
829
|
},
|
|
778
830
|
},
|
|
779
|
-
execute: async (args) => {
|
|
831
|
+
execute: async (args, context) => {
|
|
832
|
+
const waiting = new AbortController();
|
|
833
|
+
const signal = context?.signal ? AbortSignal.any([context.signal, waiting.signal]) : waiting.signal;
|
|
834
|
+
if (signal.aborted)
|
|
835
|
+
throw new Error(PANE_WAIT_CANCELLED);
|
|
780
836
|
if (!tuiProcessRef || !tuiProcessRef.isMuxMode) {
|
|
781
|
-
|
|
837
|
+
throw new Error('Terminal panes require zmux (--mux). Use run_command for shell execution when available.');
|
|
782
838
|
}
|
|
783
839
|
const paneId = Number(args.pane_id);
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
const lines = args.lines ? Number(args.lines)
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
eventPromise
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
840
|
+
if (!Number.isSafeInteger(paneId) || paneId < 0) {
|
|
841
|
+
throw new Error('pane_id must be a non-negative integer.');
|
|
842
|
+
}
|
|
843
|
+
const lines = args.lines == null ? 200 : Number(args.lines);
|
|
844
|
+
if (!Number.isSafeInteger(lines) || lines < 1 || lines > 2000)
|
|
845
|
+
throw new Error('lines must be between 1 and 2000');
|
|
846
|
+
const viewport = args.viewport === true;
|
|
847
|
+
const requestId = randomUUID();
|
|
848
|
+
// Capture this transport and its protocol version for the entire request.
|
|
849
|
+
const transport = tuiProcessRef;
|
|
850
|
+
const correlated = transport.hasCapability?.(CAPABILITY_PANE_DELIVERY_ACK) === true;
|
|
851
|
+
const read = async () => {
|
|
852
|
+
const eventPromise = waitForEvent(transport, (e) => e.type === 'pane_read' && e.pane_id === paneId
|
|
853
|
+
&& (correlated ? e.request_id === requestId
|
|
854
|
+
: (!e.request_id || e.request_id === requestId) && (e.viewport === true) === viewport), 10000, signal);
|
|
855
|
+
const sent = transport.send({
|
|
856
|
+
type: 'read_pane',
|
|
857
|
+
request_id: requestId,
|
|
858
|
+
pane_id: paneId,
|
|
859
|
+
lines,
|
|
860
|
+
viewport,
|
|
861
|
+
ansi: args.ansi === true,
|
|
862
|
+
});
|
|
863
|
+
if (!sent) {
|
|
864
|
+
eventPromise.catch(() => { });
|
|
865
|
+
waiting.abort();
|
|
866
|
+
throw new Error('Failed to send command to zmux.');
|
|
867
|
+
}
|
|
800
868
|
const event = await eventPromise;
|
|
801
869
|
if (event.type === 'pane_read') {
|
|
802
|
-
if (
|
|
870
|
+
if (event.error)
|
|
871
|
+
throw new Error(event.error);
|
|
872
|
+
if (!event.content || event.content.trim() === '')
|
|
803
873
|
return '(Terminal buffer is empty)';
|
|
804
|
-
}
|
|
805
874
|
return event.content;
|
|
806
875
|
}
|
|
807
|
-
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
return `Error: ${err.message}`;
|
|
811
|
-
}
|
|
876
|
+
throw new Error('Unexpected response from zmux.');
|
|
877
|
+
};
|
|
878
|
+
return correlated ? read() : serializeLegacyPaneRead(transport, paneId, context?.signal ?? new AbortController().signal, read);
|
|
812
879
|
},
|
|
813
880
|
};
|
|
814
881
|
export const writeTerminalPaneTool = {
|
|
882
|
+
capabilities: [ToolCapability.SHELL_EXEC],
|
|
815
883
|
definition: {
|
|
816
884
|
name: 'write_terminal_pane',
|
|
817
885
|
description: `Write input to a pane's running process. For shells and editors this sends terminal input; for ZTC child panes prefer send_message or ask_ztc_pane unless you specifically need keystroke-level control.
|
|
818
886
|
|
|
819
887
|
The input is automatically submitted (Enter/Return is appended). You do NOT need to add \\n yourself — just provide the text you want to send.
|
|
820
888
|
|
|
821
|
-
|
|
889
|
+
For raw keystrokes set submit=false; input bytes are preserved and Enter is \\r (U+000D). To recover a stuck heredoc send the Ctrl+C character (U+0003) with submit=false. Delivery acknowledges terminal input only; inspect output to determine command success.`,
|
|
822
890
|
parameters: {
|
|
823
891
|
type: 'object',
|
|
824
892
|
properties: {
|
|
@@ -838,17 +906,21 @@ If you need to send raw keystrokes WITHOUT auto-submitting (rare), set submit=fa
|
|
|
838
906
|
required: ['pane_id', 'input'],
|
|
839
907
|
},
|
|
840
908
|
},
|
|
841
|
-
execute: async (args) => {
|
|
909
|
+
execute: async (args, context) => {
|
|
910
|
+
const waiting = new AbortController();
|
|
911
|
+
const signal = context?.signal ? AbortSignal.any([context.signal, waiting.signal]) : waiting.signal;
|
|
912
|
+
if (signal.aborted)
|
|
913
|
+
throw new Error(PANE_WAIT_CANCELLED);
|
|
842
914
|
if (!tuiProcessRef || !tuiProcessRef.isMuxMode) {
|
|
843
|
-
|
|
915
|
+
throw new Error('Terminal panes require zmux (--mux). Use run_command for shell execution when available.');
|
|
844
916
|
}
|
|
845
917
|
const paneId = Number(args.pane_id);
|
|
846
|
-
if (
|
|
847
|
-
|
|
918
|
+
if (!Number.isSafeInteger(paneId) || paneId < 0) {
|
|
919
|
+
throw new Error('pane_id must be a non-negative integer.');
|
|
848
920
|
}
|
|
849
921
|
let input = String(args.input || '');
|
|
850
922
|
if (!input) {
|
|
851
|
-
|
|
923
|
+
throw new Error('input is required.');
|
|
852
924
|
}
|
|
853
925
|
const shouldSubmit = args.submit !== false && args.submit !== 'false';
|
|
854
926
|
// Normalize line endings to LF and PRESERVE interior newlines so multi-line
|
|
@@ -857,21 +929,41 @@ If you need to send raw keystrokes WITHOUT auto-submitting (rare), set submit=fa
|
|
|
857
929
|
// prompt into one line. The headless paneChild receiver delivers this as a
|
|
858
930
|
// single JSON-line message (see HeadlessPaneManager.handleWritePane), so
|
|
859
931
|
// interior newlines no longer fragment into premature submissions.
|
|
860
|
-
input = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
861
932
|
if (shouldSubmit) {
|
|
933
|
+
input = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
862
934
|
// Strip trailing newline(s); `submit` alone controls the final Enter.
|
|
863
935
|
input = input.replace(/\n+$/, '');
|
|
864
936
|
}
|
|
937
|
+
const requestId = randomUUID();
|
|
938
|
+
const confirmed = tuiProcessRef.hasCapability?.(CAPABILITY_PANE_DELIVERY_ACK) === true;
|
|
939
|
+
const eventPromise = confirmed ? waitForEvent(tuiProcessRef, event => event.type === 'pane_write_result' && event.request_id === requestId && event.pane_id === paneId, 10000, signal) : null;
|
|
865
940
|
const sent = tuiProcessRef.send({
|
|
866
941
|
type: 'write_pane',
|
|
942
|
+
request_id: requestId,
|
|
867
943
|
pane_id: paneId,
|
|
868
944
|
input,
|
|
869
945
|
submit: shouldSubmit,
|
|
870
946
|
});
|
|
871
947
|
if (!sent) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
948
|
+
eventPromise?.catch(() => { });
|
|
949
|
+
waiting.abort();
|
|
950
|
+
throw new Error('Failed to send command to zmux.');
|
|
951
|
+
}
|
|
952
|
+
if (!eventPromise)
|
|
953
|
+
return `Input sent to pane ${paneId}, not confirmed: this renderer lacks delivery acknowledgements. Inspect the pane before retrying; the command may already have run.`;
|
|
954
|
+
const event = await eventPromise;
|
|
955
|
+
if (event.type !== 'pane_write_result')
|
|
956
|
+
throw new Error('Unexpected pane acknowledgement');
|
|
957
|
+
if (event.queued) {
|
|
958
|
+
const bytes = !event.encoding_pending && event.delivered_bytes !== undefined && event.total_bytes !== undefined
|
|
959
|
+
? ` ${event.delivered_bytes} of ${event.total_bytes} bytes already delivered; the rest is queued.` : '';
|
|
960
|
+
return `Input queued for pane ${paneId}, not confirmed.${bytes} Do not resend blindly; read the pane to inspect progress.`;
|
|
961
|
+
}
|
|
962
|
+
if (!event.delivered) {
|
|
963
|
+
const bytes = event.partial && !event.cancelled ? `${event.delivered_bytes ?? 'Some'} of ${event.total_bytes ?? 'unknown'} bytes already delivered — do not resend blindly. ` : '';
|
|
964
|
+
throw new Error((event.error?.startsWith(bytes) ? '' : bytes) + (event.error || 'Pane input was not delivered'));
|
|
965
|
+
}
|
|
966
|
+
return `Input delivered to pane ${paneId}${shouldSubmit ? ' (submitted)' : ' (raw)'}. Delivery is not command completion; read the pane to inspect output.`;
|
|
875
967
|
},
|
|
876
968
|
};
|
|
877
969
|
export const closeTerminalPaneTool = {
|
|
@@ -1630,4 +1722,9 @@ export const paneTools = [
|
|
|
1630
1722
|
restartPaneTool,
|
|
1631
1723
|
closeTerminalPaneTool,
|
|
1632
1724
|
listTerminalPanesTool,
|
|
1633
|
-
]
|
|
1725
|
+
].map(tool => ({ ...tool,
|
|
1726
|
+
// read_terminal_pane returns program output, which may itself begin with "Error:";
|
|
1727
|
+
// its own failures are thrown, so only tool-authored messages are classified.
|
|
1728
|
+
isErrorResult: tool === readTerminalPaneTool ? undefined : (content) => /^Error[: ]/i.test(content),
|
|
1729
|
+
capabilities: tool.capabilities ?? ([createTerminalPaneTool, createZtcPaneTool, restartPaneTool, closeTerminalPaneTool].includes(tool)
|
|
1730
|
+
? [ToolCapability.SHELL_EXEC] : [ToolCapability.FILE_READ]), }));
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Splitting this out of index.ts avoids circular imports.
|
|
5
5
|
*/
|
|
6
6
|
import { readFileTool, writeFileTool, editFileTool, listDirectoryTool } from './file.js';
|
|
7
|
-
import { runCommandTool } from './shell.js';
|
|
7
|
+
import { runCommandTool, readCommandTool, stopCommandTool } from './shell.js';
|
|
8
8
|
import { zergQueryTool, zergTaskStatusTool, zergListTool } from './zerg.js';
|
|
9
9
|
import { zergWatchTool } from './zerg_watch.js';
|
|
10
10
|
import { searchTool } from './search.js';
|
|
@@ -31,6 +31,8 @@ export const defaultTools = [
|
|
|
31
31
|
listSkillsTool,
|
|
32
32
|
invokeSkillTool,
|
|
33
33
|
runCommandTool,
|
|
34
|
+
readCommandTool,
|
|
35
|
+
stopCommandTool,
|
|
34
36
|
zergQueryTool,
|
|
35
37
|
zergTaskStatusTool,
|
|
36
38
|
zergListTool,
|
|
@@ -13,3 +13,5 @@ export declare function summarizeCommandFailure(output: string, limit?: number):
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function shellErrorHint(exitCode: number, stderr: string, hasStdout: boolean): string | null;
|
|
15
15
|
export declare const runCommandTool: Tool;
|
|
16
|
+
export declare const readCommandTool: Tool;
|
|
17
|
+
export declare const stopCommandTool: Tool;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
2
|
import { ToolCapability } from '../runtime/capabilities.js';
|
|
3
3
|
import { runShellCommand } from '../../utils/shell.js';
|
|
4
|
+
import { startCommandJob, readCommandJob, stopCommandJob } from '../runtime/command_jobs.js';
|
|
5
|
+
import { resolveExecutionSettings } from '../../config/execution.js';
|
|
4
6
|
import { configStore } from '../../config.js';
|
|
5
7
|
import { assertTemporaryRootSafe, isPathWithinRoot } from '../../utils/temporary_write_root.js';
|
|
6
8
|
export const FAILURE_OUTPUT_LIMIT = 2000;
|
|
@@ -46,8 +48,17 @@ export function shellErrorHint(exitCode, stderr, hasStdout) {
|
|
|
46
48
|
}
|
|
47
49
|
return null;
|
|
48
50
|
}
|
|
51
|
+
function failedCommandReceipt(content) {
|
|
52
|
+
try {
|
|
53
|
+
return ['failed', 'cancelled', 'timed_out', 'interrupted'].includes(JSON.parse(content).status);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
49
59
|
// --- Shell Tool ---
|
|
50
60
|
export const runCommandTool = {
|
|
61
|
+
isErrorResult: failedCommandReceipt,
|
|
51
62
|
capabilities: [ToolCapability.SHELL_EXEC],
|
|
52
63
|
definition: {
|
|
53
64
|
name: 'run_command',
|
|
@@ -68,6 +79,8 @@ export const runCommandTool = {
|
|
|
68
79
|
type: 'string',
|
|
69
80
|
description: 'Timeout in milliseconds (default: 30000)'
|
|
70
81
|
},
|
|
82
|
+
background: { type: 'boolean', description: 'Start a managed background job. Use read_command and stop_command with its job_id; survives a turn interrupt by default.' },
|
|
83
|
+
yield_time_ms: { type: 'number', description: 'Initial background wait, 0–60000ms (default 1000).' },
|
|
71
84
|
stdin: {
|
|
72
85
|
type: 'string',
|
|
73
86
|
description: 'Optional text written to the command stdin. Prefer this over heredocs or nested quoting for multi-line payloads (e.g. git commit -F -).'
|
|
@@ -132,7 +145,7 @@ export const runCommandTool = {
|
|
|
132
145
|
const stdin = typeof args.stdin === 'string'
|
|
133
146
|
? args.stdin
|
|
134
147
|
: undefined;
|
|
135
|
-
const
|
|
148
|
+
const shellOptions = {
|
|
136
149
|
profileId,
|
|
137
150
|
profile,
|
|
138
151
|
sandbox,
|
|
@@ -140,7 +153,22 @@ export const runCommandTool = {
|
|
|
140
153
|
temporaryWriteRoot,
|
|
141
154
|
stdin,
|
|
142
155
|
env: context?.subprocessEnv,
|
|
143
|
-
}
|
|
156
|
+
};
|
|
157
|
+
if (args.background === true) {
|
|
158
|
+
if (temporaryWriteRoot)
|
|
159
|
+
throw new Error('Background commands are unavailable in temporary write-root workers. Run synchronously.');
|
|
160
|
+
if (!context?.sessionId)
|
|
161
|
+
throw new Error('Managed background commands require a session ID from the host.');
|
|
162
|
+
const wait = args.yield_time_ms === undefined ? 1000 : Number(args.yield_time_ms);
|
|
163
|
+
if (!Number.isSafeInteger(wait) || wait < 0 || wait > 60000)
|
|
164
|
+
throw new Error('yield_time_ms must be between 0 and 60000');
|
|
165
|
+
const settings = resolveExecutionSettings(config.execution);
|
|
166
|
+
const receipt = await startCommandJob({ sessionId: context.sessionId, command, cwd, timeoutMs,
|
|
167
|
+
shell: shellOptions, maxJobs: settings.maxBackgroundJobs,
|
|
168
|
+
signal: settings.interruptBackgroundJobs ? context.signal : undefined });
|
|
169
|
+
return JSON.stringify(await readCommandJob(context.sessionId, receipt.job_id, 0, wait, context.signal));
|
|
170
|
+
}
|
|
171
|
+
const result = await runShellCommand(command, cwd, context?.signal, timeoutMs, shellOptions);
|
|
144
172
|
// Handle non-zero exit codes
|
|
145
173
|
if (result.exitCode !== 0) {
|
|
146
174
|
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
@@ -160,3 +188,30 @@ export const runCommandTool = {
|
|
|
160
188
|
});
|
|
161
189
|
}
|
|
162
190
|
};
|
|
191
|
+
export const readCommandTool = {
|
|
192
|
+
isErrorResult: failedCommandReceipt,
|
|
193
|
+
capabilities: [ToolCapability.FILE_READ],
|
|
194
|
+
definition: {
|
|
195
|
+
name: 'read_command', description: 'Read a managed command receipt and up to 16KB of incremental output. Cursor is a byte offset returned by the previous receipt. Wait is bounded; status running does not mean success.',
|
|
196
|
+
parameters: { type: 'object', properties: {
|
|
197
|
+
job_id: { type: 'string', description: 'Session-owned job ID from run_command.' }, cursor: { type: 'number', description: 'Byte cursor from the previous read (default 0).' }, wait_ms: { type: 'number', description: '0–60000ms; waits for completion or deadline.' },
|
|
198
|
+
}, required: ['job_id'] },
|
|
199
|
+
},
|
|
200
|
+
execute: async (args, context) => {
|
|
201
|
+
if (!context?.sessionId)
|
|
202
|
+
throw new Error('Managed commands require a session ID.');
|
|
203
|
+
return JSON.stringify(await readCommandJob(context.sessionId, String(args.job_id), Number(args.cursor ?? 0), Number(args.wait_ms ?? 0), context.signal));
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
export const stopCommandTool = {
|
|
207
|
+
capabilities: [ToolCapability.SHELL_EXEC],
|
|
208
|
+
definition: {
|
|
209
|
+
name: 'stop_command', description: 'Stop an owned local command process group and return its final receipt. Remote jobs must provide their own cleanup.',
|
|
210
|
+
parameters: { type: 'object', properties: { job_id: { type: 'string', description: 'Session-owned job ID from run_command.' } }, required: ['job_id'] },
|
|
211
|
+
},
|
|
212
|
+
execute: async (args, context) => {
|
|
213
|
+
if (!context?.sessionId)
|
|
214
|
+
throw new Error('Managed commands require a session ID.');
|
|
215
|
+
return JSON.stringify(await stopCommandJob(context.sessionId, String(args.job_id)));
|
|
216
|
+
},
|
|
217
|
+
};
|
|
@@ -6,6 +6,7 @@ export interface ToolContext {
|
|
|
6
6
|
cwd: string;
|
|
7
7
|
/** The model-provided tool-use id that originated this execution. */
|
|
8
8
|
toolCallId?: string;
|
|
9
|
+
sessionId?: string;
|
|
9
10
|
signal?: AbortSignal;
|
|
10
11
|
temporaryWriteRoot?: string;
|
|
11
12
|
/** Exact environment for shell-tool subprocesses; undefined inherits legacy ambient env. */
|
|
@@ -17,6 +18,9 @@ export interface ToolContext {
|
|
|
17
18
|
}
|
|
18
19
|
export interface Tool {
|
|
19
20
|
definition: ToolDefinition;
|
|
21
|
+
/** Explicit scheduling opt-in; only read effects may run concurrently. */
|
|
22
|
+
parallelSafe?: boolean;
|
|
23
|
+
isErrorResult?: (content: string) => boolean;
|
|
20
24
|
capabilities?: ToolCapability[];
|
|
21
25
|
/** Host scheduling boundary. When omitted, capabilities are mapped
|
|
22
26
|
* conservatively by `resolveToolEffect`. */
|
package/dist/app/agent_runner.js
CHANGED
|
@@ -151,7 +151,7 @@ export class AgentRunLifecycle {
|
|
|
151
151
|
this.abortController = controller;
|
|
152
152
|
const isActive = () => this.activeRunId === runId;
|
|
153
153
|
const cleanup = agent.on((event) => {
|
|
154
|
-
if (!isActive())
|
|
154
|
+
if (!isActive() && (event.type !== 'history_checkpoint' || this.activeRunId !== null))
|
|
155
155
|
return;
|
|
156
156
|
onEvent(event);
|
|
157
157
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Message } from '../types.js';
|
|
2
|
+
export interface TranscriptIteration {
|
|
3
|
+
start: number;
|
|
4
|
+
toolStart?: number;
|
|
5
|
+
}
|
|
6
|
+
/** Reconcile model history with the live display, keeping each iteration's arrival order. */
|
|
7
|
+
export declare function reconcileOrderedTranscript(options: {
|
|
8
|
+
live: readonly Message[];
|
|
9
|
+
ordered: readonly Message[];
|
|
10
|
+
iterations: readonly TranscriptIteration[];
|
|
11
|
+
toolIterations: ReadonlyMap<string, number>;
|
|
12
|
+
finalMessage?: Message;
|
|
13
|
+
}): Message[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isPersistableAssistantMessage } from '../utils/assistant_message.js';
|
|
2
|
+
/** Reconcile model history with the live display, keeping each iteration's arrival order. */
|
|
3
|
+
export function reconcileOrderedTranscript(options) {
|
|
4
|
+
const { live, ordered, iterations, toolIterations, finalMessage } = options;
|
|
5
|
+
if (!iterations.length)
|
|
6
|
+
return [...live];
|
|
7
|
+
const byIteration = iterations.map(() => []);
|
|
8
|
+
let current = 0;
|
|
9
|
+
for (const message of ordered) {
|
|
10
|
+
if (message.toolCalls?.length)
|
|
11
|
+
current = toolIterations.get(message.toolCalls[0].id) ?? current;
|
|
12
|
+
byIteration[current].push(message);
|
|
13
|
+
}
|
|
14
|
+
const consumed = new Set(ordered.filter(message => message.role === 'user').map(message => message.id));
|
|
15
|
+
const output = live.slice(0, iterations[0].start);
|
|
16
|
+
for (let index = 0; index < iterations.length; index++) {
|
|
17
|
+
const frame = iterations[index];
|
|
18
|
+
const end = iterations[index + 1]?.start ?? live.length;
|
|
19
|
+
const segment = live.slice(frame.start, end).filter(message => !consumed.has(message.id));
|
|
20
|
+
const canonical = byIteration[index].find(message => message.toolCalls?.length);
|
|
21
|
+
if (canonical) {
|
|
22
|
+
const calls = canonical.toolCalls.map(({ result: _result, error: _error, ...call }) => call);
|
|
23
|
+
const beforeTools = live.slice(frame.start, frame.toolStart ?? end);
|
|
24
|
+
const anchorId = beforeTools.filter(isPersistableAssistantMessage).at(-1)?.id;
|
|
25
|
+
const anchor = segment.findIndex(message => message.id === anchorId);
|
|
26
|
+
if (anchor >= 0) {
|
|
27
|
+
segment[anchor] = { ...segment[anchor], toolCalls: calls,
|
|
28
|
+
metadata: { ...segment[anchor].metadata, ...canonical.metadata, toolCallsInTranscript: true } };
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// A tool-only response has no answer bubble to carry its API tool_use.
|
|
32
|
+
// Keep this record before the live trace without moving Thinking blocks.
|
|
33
|
+
const trace = segment.findIndex(message => message.role === 'tool');
|
|
34
|
+
const alreadyDisplayed = segment.filter(isPersistableAssistantMessage).map(message => message.content).join('') === canonical.content;
|
|
35
|
+
segment.splice(trace < 0 ? segment.length : trace, 0, { ...canonical,
|
|
36
|
+
content: alreadyDisplayed ? '[Using tools...]' : canonical.content,
|
|
37
|
+
toolCalls: calls, metadata: { ...canonical.metadata, toolCallsInTranscript: true } });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
output.push(...segment, ...byIteration[index].filter(message => message !== canonical));
|
|
41
|
+
}
|
|
42
|
+
if (finalMessage) {
|
|
43
|
+
const finalFrame = iterations.length - 1;
|
|
44
|
+
const finalLive = live.slice(iterations[finalFrame].start).filter(isPersistableAssistantMessage);
|
|
45
|
+
const anchorId = finalLive.at(-1)?.id;
|
|
46
|
+
const anchor = output.findIndex(message => message.id === anchorId);
|
|
47
|
+
if (anchor >= 0) {
|
|
48
|
+
output[anchor] = { ...output[anchor], metadata: { ...output[anchor].metadata, ...finalMessage.metadata } };
|
|
49
|
+
}
|
|
50
|
+
else if (!byIteration[finalFrame].some(message => message.role === 'assistant' && message.content === finalMessage.content)) {
|
|
51
|
+
output.push(finalMessage);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return output;
|
|
55
|
+
}
|
package/dist/cli_ratatui.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RatatuiApp — the interactive TUI application class.
|
|
3
|
-
*
|
|
4
|
-
* Uses the Rust/Ratatui renderer for terminal rendering with proper
|
|
5
|
-
* differential rendering. The CLI entry point that parses arguments
|
|
6
|
-
* and launches this class lives in cli_main.ts.
|
|
7
|
-
*/
|
|
8
1
|
import { TuiProcess, NullTuiProcess, HeadlessPaneManager } from './ui/ratatui/index.js';
|
|
9
2
|
import { getMirrorCommandUsages, isRichSlashUsage } from './agent/commands/mirror_usages.js';
|
|
10
3
|
import { type Session } from './session.js';
|
|
@@ -909,6 +902,8 @@ export declare class RatatuiApp {
|
|
|
909
902
|
* its restricted tool surface must take precedence over that pane default.
|
|
910
903
|
*/
|
|
911
904
|
private getEffectiveAgentMode;
|
|
905
|
+
private commandJobsUnsubscribe;
|
|
906
|
+
private commandJobStates;
|
|
912
907
|
private createAgent;
|
|
913
908
|
private awaitAgentBind;
|
|
914
909
|
private bindAgentStore;
|