@zergai/ztc 0.2.7-dev.36236183737 → 0.3.1-dev.36465749722
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
package/bin/zmux-darwin-arm64
CHANGED
|
Binary file
|
package/bin/ztc-tui-darwin-arm64
CHANGED
|
Binary file
|
package/bin/zterm-darwin-arm64
CHANGED
|
Binary file
|
package/dist/App.js
CHANGED
|
@@ -527,7 +527,21 @@ export const App = () => {
|
|
|
527
527
|
return;
|
|
528
528
|
try {
|
|
529
529
|
streamedResponse.current = false;
|
|
530
|
-
const
|
|
530
|
+
const priorIds = new Set(requestMessages.map(message => message.id));
|
|
531
|
+
const stopCheckpoints = runAgent.on(event => {
|
|
532
|
+
if (event.type !== 'history_checkpoint')
|
|
533
|
+
return;
|
|
534
|
+
const ordered = event.messages;
|
|
535
|
+
setMessages(prev => [...prev.filter(message => priorIds.has(message.id)), ...ordered,
|
|
536
|
+
...prev.filter(message => !priorIds.has(message.id) && (message.role === 'system' || message.role === 'tool'))]);
|
|
537
|
+
});
|
|
538
|
+
let result;
|
|
539
|
+
try {
|
|
540
|
+
result = await runAgent.run(requestMessages, signal);
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
stopCheckpoints();
|
|
544
|
+
}
|
|
531
545
|
if (isActive && !isActive())
|
|
532
546
|
return;
|
|
533
547
|
if (!streamedResponse.current) {
|
|
@@ -539,7 +553,7 @@ export const App = () => {
|
|
|
539
553
|
addMessage({
|
|
540
554
|
role: 'assistant',
|
|
541
555
|
content: result.content,
|
|
542
|
-
toolCalls: result.toolCalls.length > 0 ? result.toolCalls : undefined,
|
|
556
|
+
toolCalls: result.turnMessages?.length ? undefined : result.toolCalls.length > 0 ? result.toolCalls : undefined,
|
|
543
557
|
// Keep the provider's reasoning trace alongside usage: reasoning
|
|
544
558
|
// models expect the assistant message replayed with its
|
|
545
559
|
// reasoning_content, and dropping it here strips every later turn.
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Message, ToolCall, AgentEvent } from '../types.js';
|
|
|
2
2
|
import { AgentBackend, BackendRequest, TokenUsage } from './backends/index.js';
|
|
3
3
|
import type { ClientToolCallResult } from './backends/types.js';
|
|
4
4
|
import type { ApiRetrySettings } from '../config/types.js';
|
|
5
|
+
import { type ExecutionSettings } from '../config/execution.js';
|
|
6
|
+
import { type ExecutionContext } from './runtime/execution_context.js';
|
|
5
7
|
import { Policy } from './runtime/policy.js';
|
|
6
8
|
import { Tracer } from './runtime/tracing.js';
|
|
7
9
|
import type { Tool } from './tools/types.js';
|
|
@@ -26,6 +28,14 @@ export interface AgentConfig {
|
|
|
26
28
|
allow_fallbacks: boolean;
|
|
27
29
|
};
|
|
28
30
|
maxIterations?: number;
|
|
31
|
+
execution?: ExecutionSettings;
|
|
32
|
+
sessionId?: string;
|
|
33
|
+
runtimeFacts?: {
|
|
34
|
+
mode?: string;
|
|
35
|
+
mux?: boolean;
|
|
36
|
+
configScope?: string;
|
|
37
|
+
outputLimitSource?: string;
|
|
38
|
+
};
|
|
29
39
|
/**
|
|
30
40
|
* Retry/timeout policy for LLM requests. Renderer hosts normally leave this
|
|
31
41
|
* undefined so the config-file setting applies; an explicit value wins.
|
|
@@ -54,8 +64,10 @@ export interface AgentConfig {
|
|
|
54
64
|
*/
|
|
55
65
|
maxConsecutiveAutoContinues?: number;
|
|
56
66
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
67
|
+
* Local task sessions: append current execution facts and any explicitly
|
|
68
|
+
* pinned goal to request.system. User messages are never automatically
|
|
69
|
+
* pinned. Hosted multi-turn chat should set false, which leaves
|
|
70
|
+
* request.system exactly as configured.
|
|
59
71
|
*/
|
|
60
72
|
pinTaskHeader?: boolean;
|
|
61
73
|
/**
|
|
@@ -105,7 +117,7 @@ export interface AgentRunResult {
|
|
|
105
117
|
usage?: TokenUsage;
|
|
106
118
|
stopReason: 'complete' | 'interactive' | 'max_iterations';
|
|
107
119
|
reasoning?: string;
|
|
108
|
-
/** Ordered model-facing messages produced during
|
|
120
|
+
/** Ordered model-facing messages produced during every turn. */
|
|
109
121
|
turnMessages?: Message[];
|
|
110
122
|
}
|
|
111
123
|
export interface HarnessToolCallResult {
|
|
@@ -240,11 +252,9 @@ export declare class Agent {
|
|
|
240
252
|
*/
|
|
241
253
|
private _taskHeader;
|
|
242
254
|
/**
|
|
243
|
-
* Drop the pinned task
|
|
244
|
-
* message. Wired into the /clear-only path (clearTransientState), never into
|
|
255
|
+
* Drop the explicitly pinned task for the next conversation. Wired into the /clear-only path (clearTransientState), never into
|
|
245
256
|
* /compact — the pin must survive compaction (that is the whole point of R7).
|
|
246
|
-
* Null
|
|
247
|
-
* "no task found."
|
|
257
|
+
* Null represents no explicitly pinned goal.
|
|
248
258
|
*/
|
|
249
259
|
clearPinnedTask(): void;
|
|
250
260
|
/**
|
|
@@ -257,10 +267,11 @@ export declare class Agent {
|
|
|
257
267
|
* for the whole run. A driver that starts or switches a goal calls this so
|
|
258
268
|
* request.system and the goal's per-turn re-injection name the same objective.
|
|
259
269
|
*
|
|
260
|
-
* A blank value
|
|
270
|
+
* A blank value clears the pin, matching clearPinnedTask.
|
|
261
271
|
*/
|
|
262
272
|
setPinnedTask(task: string): void;
|
|
263
273
|
/** Append the pinned task header (R7) to the static system prompt. */
|
|
274
|
+
getExecutionContext(): ExecutionContext;
|
|
264
275
|
private systemWithTaskHeader;
|
|
265
276
|
/**
|
|
266
277
|
* Execute one terminal turn. A host binding makes the remote conversation the
|
|
@@ -268,6 +279,7 @@ export declare class Agent {
|
|
|
268
279
|
* the same core loop used by unhosted ZTC.
|
|
269
280
|
*/
|
|
270
281
|
run(messages: Message[], signal?: AbortSignal, takeSteering?: () => Message[]): Promise<AgentRunResult>;
|
|
282
|
+
private runBound;
|
|
271
283
|
private runCore;
|
|
272
284
|
/**
|
|
273
285
|
* Release the backend. The backend lives as long as the Agent — the run loop
|
package/dist/agent/agent.js
CHANGED
|
@@ -7,6 +7,11 @@ import { mergeTokenUsage } from './backends/token_usage.js';
|
|
|
7
7
|
import { isTransportCodeError, isTransportMessageError } from './backends/transport_errors.js';
|
|
8
8
|
import { inferDefaultProvider, getDefaultApiKeyForProvider, getDefaultModelForProvider } from '../config/defaults.js';
|
|
9
9
|
import { resolveApiRetrySettings } from '../config/api_retry.js';
|
|
10
|
+
import { resolveExecutionSettings } from '../config/execution.js';
|
|
11
|
+
import { createRecoveryGuard } from './runtime/recovery.js';
|
|
12
|
+
import { listCommandJobs, interruptCommandJobs } from './runtime/command_jobs.js';
|
|
13
|
+
import { describeExecutionContext } from './runtime/execution_context.js';
|
|
14
|
+
import { scheduleToolBatch, commandStillRunning } from './runtime/tool_scheduler.js';
|
|
10
15
|
import { AllowAllPolicy } from './runtime/policy.js';
|
|
11
16
|
import { NoopTracer } from './runtime/tracing.js';
|
|
12
17
|
import { defaultTools as baseDefaultTools } from './tools/registry.js';
|
|
@@ -208,6 +213,9 @@ export class Agent {
|
|
|
208
213
|
maxTokens: config.maxTokens,
|
|
209
214
|
routePlan: config.routePlan,
|
|
210
215
|
maxIterations: config.maxIterations || 10,
|
|
216
|
+
execution: resolveExecutionSettings(config.execution),
|
|
217
|
+
sessionId: config.sessionId ?? randomUUID(),
|
|
218
|
+
runtimeFacts: config.runtimeFacts ?? {},
|
|
211
219
|
apiRetry: resolveApiRetrySettings({ apiRetry: config.apiRetry }),
|
|
212
220
|
llmRequestTimeoutMs: resolveLlmRequestTimeoutMs(config.llmRequestTimeoutMs, config.apiRetry?.requestTimeoutMs),
|
|
213
221
|
tools: config.tools || (ambientDiscovery ? defaultTools : []),
|
|
@@ -574,6 +582,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
574
582
|
.map(item => ({
|
|
575
583
|
tool_use_id: item.tool_use_id,
|
|
576
584
|
content: typeof item.content === 'string' ? item.content : String(item.content ?? ''),
|
|
585
|
+
is_error: item.is_error,
|
|
577
586
|
}));
|
|
578
587
|
}
|
|
579
588
|
catch {
|
|
@@ -668,7 +677,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
668
677
|
content = content.slice(0, SYNTH_RESULT_CAP) + `\n[truncated — ${content.length} chars total]`;
|
|
669
678
|
}
|
|
670
679
|
emittedToolResultIds.add(tc.id);
|
|
671
|
-
return { tool_use_id: tc.id, content };
|
|
680
|
+
return { tool_use_id: tc.id, content, is_error: Boolean(tc.error) || tc.result == null };
|
|
672
681
|
});
|
|
673
682
|
if (toolResults.length === 0)
|
|
674
683
|
continue;
|
|
@@ -687,12 +696,30 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
687
696
|
}
|
|
688
697
|
buildRequest(messages, availableTools = this.config.tools) {
|
|
689
698
|
// Repair any orphaned tool_use blocks before building request
|
|
699
|
+
if (this.config.pinTaskHeader) {
|
|
700
|
+
const facts = describeExecutionContext({ ...this.getExecutionContext(), tools: availableTools.map(tool => tool.definition.name) });
|
|
701
|
+
const latestFacts = [...messages].reverse().find(message => typeof message.metadata?.executionContext === 'string')?.metadata?.executionContext;
|
|
702
|
+
if (facts !== latestFacts) {
|
|
703
|
+
const latestUser = [...messages].reverse().find(message => message.role === 'user');
|
|
704
|
+
if (latestUser)
|
|
705
|
+
latestUser.metadata = { ...latestUser.metadata, executionContext: facts };
|
|
706
|
+
}
|
|
707
|
+
}
|
|
690
708
|
const repairedMessages = this.repairConversationHistory(messages);
|
|
709
|
+
// Facts are attached to the user/tool-result turn that observed them. Keep
|
|
710
|
+
// earlier snapshots immutable so provider threads can reuse their prefix.
|
|
711
|
+
const contextMessages = this.config.pinTaskHeader ? repairedMessages.flatMap(message => {
|
|
712
|
+
const facts = message.metadata?.executionContext;
|
|
713
|
+
return typeof facts === 'string' ? [message, {
|
|
714
|
+
id: `${message.id}_execution`, role: 'user', content: facts, timestamp: message.timestamp,
|
|
715
|
+
metadata: { synthetic: true },
|
|
716
|
+
}] : [message];
|
|
717
|
+
}) : repairedMessages;
|
|
691
718
|
return {
|
|
692
719
|
model: this.config.model,
|
|
693
720
|
maxTokens: this.config.maxTokens,
|
|
694
721
|
system: this.systemWithTaskHeader(messages),
|
|
695
|
-
messages: this.formatMessages(
|
|
722
|
+
messages: this.formatMessages(contextMessages),
|
|
696
723
|
tools: getRequestToolDefinitions(availableTools, this.discoveredTools, this.config.deferToolSchemas !== false, this.alwaysLoadedToolNames),
|
|
697
724
|
// A container pin identifies a cached prompt prefix, so the transport has to
|
|
698
725
|
// know when this conversation is no longer sending the prefix its container
|
|
@@ -918,11 +945,9 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
918
945
|
*/
|
|
919
946
|
_taskHeader = null;
|
|
920
947
|
/**
|
|
921
|
-
* Drop the pinned task
|
|
922
|
-
* message. Wired into the /clear-only path (clearTransientState), never into
|
|
948
|
+
* Drop the explicitly pinned task for the next conversation. Wired into the /clear-only path (clearTransientState), never into
|
|
923
949
|
* /compact — the pin must survive compaction (that is the whole point of R7).
|
|
924
|
-
* Null
|
|
925
|
-
* "no task found."
|
|
950
|
+
* Null represents no explicitly pinned goal.
|
|
926
951
|
*/
|
|
927
952
|
clearPinnedTask() {
|
|
928
953
|
this._taskHeader = null;
|
|
@@ -937,23 +962,27 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
937
962
|
* for the whole run. A driver that starts or switches a goal calls this so
|
|
938
963
|
* request.system and the goal's per-turn re-injection name the same objective.
|
|
939
964
|
*
|
|
940
|
-
* A blank value
|
|
965
|
+
* A blank value clears the pin, matching clearPinnedTask.
|
|
941
966
|
*/
|
|
942
967
|
setPinnedTask(task) {
|
|
943
968
|
const trimmed = task.trim();
|
|
944
969
|
this._taskHeader = trimmed.length > 0 ? trimmed : null;
|
|
945
970
|
}
|
|
946
971
|
/** Append the pinned task header (R7) to the static system prompt. */
|
|
947
|
-
|
|
948
|
-
|
|
972
|
+
getExecutionContext() {
|
|
973
|
+
return {
|
|
974
|
+
mode: this.config.runtimeFacts.mode ?? 'normal', cwd: this._cwd,
|
|
975
|
+
tools: this.getConfiguredToolNames(), mux: this.config.runtimeFacts.mux ?? false,
|
|
976
|
+
configScope: this.config.runtimeFacts.configScope ?? 'host configuration',
|
|
977
|
+
maxTokens: this.config.maxTokens,
|
|
978
|
+
outputLimitSource: this.config.runtimeFacts.outputLimitSource ?? (this.config.maxTokens === undefined ? 'provider default' : 'explicit configuration'),
|
|
979
|
+
settings: resolveExecutionSettings(this.config.execution), jobs: listCommandJobs(this.config.sessionId),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
systemWithTaskHeader(_messages) {
|
|
983
|
+
// Hosted chat owns its system prompt: no local execution facts and no pin.
|
|
984
|
+
if (!this.config.pinTaskHeader)
|
|
949
985
|
return this.config.systemPrompt;
|
|
950
|
-
}
|
|
951
|
-
if (this._taskHeader === null) {
|
|
952
|
-
const firstUser = messages.find(m => m.role === 'user' && !m.metadata?.synthetic && !m.metadata?.toolResult && typeof m.content === 'string' && m.content.trim().length > 0);
|
|
953
|
-
this._taskHeader = firstUser ? firstUser.content.trim().slice(0, 2000) : '';
|
|
954
|
-
}
|
|
955
|
-
// Shared loop_policy formatting (identical to SubAgent.run); returns the
|
|
956
|
-
// base prompt unchanged when there is no captured task.
|
|
957
986
|
return pinnedTaskSystem(this.config.systemPrompt, this._taskHeader);
|
|
958
987
|
}
|
|
959
988
|
/**
|
|
@@ -962,6 +991,17 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
962
991
|
* the same core loop used by unhosted ZTC.
|
|
963
992
|
*/
|
|
964
993
|
async run(messages, signal, takeSteering) {
|
|
994
|
+
const onInterrupt = () => { if (this.config.execution.interruptBackgroundJobs)
|
|
995
|
+
void interruptCommandJobs(this.config.sessionId); };
|
|
996
|
+
signal?.addEventListener('abort', onInterrupt, { once: true });
|
|
997
|
+
try {
|
|
998
|
+
return await this.runBound(messages, signal, takeSteering);
|
|
999
|
+
}
|
|
1000
|
+
finally {
|
|
1001
|
+
signal?.removeEventListener('abort', onInterrupt);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async runBound(messages, signal, takeSteering) {
|
|
965
1005
|
const binding = this.#conversationBinding;
|
|
966
1006
|
if (!binding)
|
|
967
1007
|
return this.runCore(messages, signal, takeSteering);
|
|
@@ -1048,7 +1088,6 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1048
1088
|
}
|
|
1049
1089
|
const allToolCalls = [];
|
|
1050
1090
|
const turnMessages = [];
|
|
1051
|
-
let acceptedSteering = false;
|
|
1052
1091
|
let iterations = 0;
|
|
1053
1092
|
// Each turn gets its own auto-continue budget; a long-running Agent must
|
|
1054
1093
|
// not inherit one spent by the previous turn.
|
|
@@ -1072,6 +1111,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1072
1111
|
// reproduced a byte-identical result as non-progressing (F8 churn / F1
|
|
1073
1112
|
// re-fire). Result-equality (not raw repeat count) spares legitimate polling.
|
|
1074
1113
|
const applyRepeatGuard = createRepeatGuard();
|
|
1114
|
+
const recovery = createRecoveryGuard(this.config.execution);
|
|
1075
1115
|
try {
|
|
1076
1116
|
while (iterations < this.config.maxIterations) {
|
|
1077
1117
|
if (signal?.aborted) {
|
|
@@ -1189,10 +1229,6 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1189
1229
|
stopReason = 'complete';
|
|
1190
1230
|
break;
|
|
1191
1231
|
}
|
|
1192
|
-
// Claude requests multiple tool_use blocks in a single response only
|
|
1193
|
-
// when they are independent, so running them concurrently is safe and
|
|
1194
|
-
// significantly faster for multi-file reads, searches, etc.
|
|
1195
|
-
//
|
|
1196
1232
|
// Per-batch dedup for `recall_tool_response`: two recalls of the same
|
|
1197
1233
|
// inner tool_use_id in one assistant turn share a single execute. The
|
|
1198
1234
|
// outer toolBlock.id pairing is preserved for each block so the
|
|
@@ -1210,7 +1246,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1210
1246
|
data: { tool: toolBlock.name },
|
|
1211
1247
|
});
|
|
1212
1248
|
const guarded = applyRepeatGuard(toolBlock.name, toolBlock.input, content);
|
|
1213
|
-
return { toolCall, toolResult: { tool_use_id: toolBlock.id, content: guarded } };
|
|
1249
|
+
return { toolCall, toolResult: { tool_use_id: toolBlock.id, content: guarded, is_error: false } };
|
|
1214
1250
|
};
|
|
1215
1251
|
const recordToolError = (toolBlock, toolCall, error, effectiveArgs) => {
|
|
1216
1252
|
toolCall.completedAt = new Date();
|
|
@@ -1229,6 +1265,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1229
1265
|
toolResult: {
|
|
1230
1266
|
tool_use_id: toolBlock.id,
|
|
1231
1267
|
content: applyRepeatGuard(toolBlock.name, toolBlock.input, `Error: ${error}`),
|
|
1268
|
+
is_error: true,
|
|
1232
1269
|
},
|
|
1233
1270
|
};
|
|
1234
1271
|
};
|
|
@@ -1245,7 +1282,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1245
1282
|
// It must be the only executable call in this provider response; the
|
|
1246
1283
|
// current agent is rebuilt before any subsequent iteration.
|
|
1247
1284
|
const exclusiveTransitionId = toolUseBlocks.find((toolBlock) => toolBlock.name === 'plan_mode')?.id ?? null;
|
|
1248
|
-
const
|
|
1285
|
+
const combined = await scheduleToolBatch(toolUseBlocks, this.config.tools, this.config.execution, async (toolBlock, skipReason) => {
|
|
1249
1286
|
const toolCall = {
|
|
1250
1287
|
id: toolBlock.id,
|
|
1251
1288
|
name: toolBlock.name,
|
|
@@ -1259,6 +1296,8 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1259
1296
|
data: { tool: toolBlock.name }
|
|
1260
1297
|
});
|
|
1261
1298
|
this.emit({ type: 'tool_start', tool: toolBlock.name, args: toolBlock.input, toolCallId: toolBlock.id });
|
|
1299
|
+
if (skipReason)
|
|
1300
|
+
return recordToolError(toolBlock, toolCall, skipReason);
|
|
1262
1301
|
if (exclusiveTransitionId !== null
|
|
1263
1302
|
&& toolBlock.id !== exclusiveTransitionId) {
|
|
1264
1303
|
return recordToolError(toolBlock, toolCall, 'Skipped because plan_mode is an exclusive safety transition; retry after the plan agent is active.');
|
|
@@ -1298,6 +1337,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1298
1337
|
context: {
|
|
1299
1338
|
cwd: this._cwd,
|
|
1300
1339
|
toolCallId: toolBlock.id,
|
|
1340
|
+
sessionId: this.config.sessionId,
|
|
1301
1341
|
signal,
|
|
1302
1342
|
toolResponseStore: this.toolResponseStore,
|
|
1303
1343
|
discoveredTools: this.discoveredTools,
|
|
@@ -1321,6 +1361,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1321
1361
|
context: {
|
|
1322
1362
|
cwd: this._cwd,
|
|
1323
1363
|
toolCallId: toolBlock.id,
|
|
1364
|
+
sessionId: this.config.sessionId,
|
|
1324
1365
|
signal,
|
|
1325
1366
|
toolResponseStore: this.toolResponseStore,
|
|
1326
1367
|
discoveredTools: this.discoveredTools,
|
|
@@ -1344,21 +1385,17 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1344
1385
|
const error = err.message;
|
|
1345
1386
|
return recordToolError(toolBlock, toolCall, error);
|
|
1346
1387
|
}
|
|
1347
|
-
});
|
|
1348
|
-
const combined = await Promise.all(toolPromises);
|
|
1388
|
+
}, result => result.toolCall.status === 'error' || commandStillRunning(result.toolResult.content), signal, exclusiveTransitionId);
|
|
1349
1389
|
allToolCalls.push(...combined.map(r => r.toolCall));
|
|
1350
1390
|
const partial = Boolean(signal?.aborted);
|
|
1351
1391
|
const toolResults = combined.map(r => {
|
|
1352
1392
|
const { rendered } = this.toolResponseStore.put(r.toolResult.tool_use_id, r.toolCall.name, r.toolResult.content, partial, {
|
|
1353
1393
|
sourcePath: extractSourcePath(r.toolCall.name, r.toolCall.args),
|
|
1354
1394
|
dedupable: r.toolCall.status === 'complete',
|
|
1395
|
+
isError: r.toolResult.is_error,
|
|
1355
1396
|
});
|
|
1356
|
-
return { tool_use_id: r.toolResult.tool_use_id, content: rendered };
|
|
1397
|
+
return { tool_use_id: r.toolResult.tool_use_id, content: rendered, is_error: r.toolResult.is_error };
|
|
1357
1398
|
});
|
|
1358
|
-
// Check abort after tool execution so cancel takes effect immediately
|
|
1359
|
-
if (signal?.aborted) {
|
|
1360
|
-
throw new AgentError('Cancelled', 'CANCELLED');
|
|
1361
|
-
}
|
|
1362
1399
|
// Real work happened, so the turn has demonstrably not stalled on
|
|
1363
1400
|
// narration: give it a fresh continuation budget.
|
|
1364
1401
|
this._consecutiveAutoContinues = 0;
|
|
@@ -1367,14 +1404,6 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1367
1404
|
.filter(this.isTextBlock)
|
|
1368
1405
|
.map(b => b.text)
|
|
1369
1406
|
.join('\n');
|
|
1370
|
-
// Stream any text content that came with tool calls (important for user feedback!)
|
|
1371
|
-
if (textContent && textContent.trim()) {
|
|
1372
|
-
if (!this._lastResponseStreamed)
|
|
1373
|
-
await this.emitStreamText(textContent, signal);
|
|
1374
|
-
// Last non-empty text wins — empty iterations preserve the prior value
|
|
1375
|
-
// so maxIterations exhaustion still has something to return.
|
|
1376
|
-
lastToolTurnText = textContent;
|
|
1377
|
-
}
|
|
1378
1407
|
const toolUseMessage = {
|
|
1379
1408
|
id: `assistant_${Date.now()}`,
|
|
1380
1409
|
role: 'assistant',
|
|
@@ -1394,6 +1423,29 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1394
1423
|
};
|
|
1395
1424
|
messages.push(toolResultMessage);
|
|
1396
1425
|
turnMessages.push(toolResultMessage);
|
|
1426
|
+
this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
|
|
1427
|
+
if (signal?.aborted)
|
|
1428
|
+
throw new AgentError('Cancelled', 'CANCELLED');
|
|
1429
|
+
// Stream any text content that came with tool calls (important for user feedback!)
|
|
1430
|
+
if (textContent && textContent.trim() && !signal?.aborted) {
|
|
1431
|
+
if (!this._lastResponseStreamed)
|
|
1432
|
+
await this.emitStreamText(textContent, signal);
|
|
1433
|
+
// Last non-empty text wins — empty iterations preserve the prior value
|
|
1434
|
+
// so maxIterations exhaustion still has something to return.
|
|
1435
|
+
lastToolTurnText = textContent;
|
|
1436
|
+
}
|
|
1437
|
+
const recoveryAction = recovery(combined.map(result => ({ name: result.toolCall.name,
|
|
1438
|
+
content: result.toolResult.content, isError: result.toolResult.is_error })));
|
|
1439
|
+
if (recoveryAction === 'stop')
|
|
1440
|
+
throw new AgentError('Automatic recovery budget exhausted after repeated tool failures. Inspect the retained results and current capabilities before retrying.', 'RECOVERY_EXHAUSTED');
|
|
1441
|
+
if (recoveryAction === 'recover') {
|
|
1442
|
+
const recoveryMessage = { id: `recovery_${randomUUID()}`, role: 'user',
|
|
1443
|
+
content: '[Recovery] Repeated failures share the same cause. Stop that sequence. Inspect current capabilities and active jobs, then make one changed recovery attempt. Use file/stdin tools for scripts and raw Ctrl+C for stuck shell input, then read the pane in the same response or immediately afterward. A silent running-job poll allows one further inspection before stopping; do not repeat empty polls. Continue authorized work; use ask_user only for a concrete missing prerequisite. Do not repeat an acknowledgement as proof of success.',
|
|
1444
|
+
metadata: { synthetic: true }, timestamp: new Date() };
|
|
1445
|
+
messages.push(recoveryMessage);
|
|
1446
|
+
turnMessages.push(recoveryMessage);
|
|
1447
|
+
this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
|
|
1448
|
+
}
|
|
1397
1449
|
// An explicit terminal tool may supply the answer after its bookkeeping
|
|
1398
1450
|
// succeeds. Keep the tool-use/result pair above for conversation replay.
|
|
1399
1451
|
if (combined.length === 1 && combined[0].toolCall.status === 'complete') {
|
|
@@ -1432,7 +1484,6 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1432
1484
|
if (steering.length > 0) {
|
|
1433
1485
|
messages.push(...steering);
|
|
1434
1486
|
turnMessages.push(...steering);
|
|
1435
|
-
acceptedSteering = true;
|
|
1436
1487
|
}
|
|
1437
1488
|
}
|
|
1438
1489
|
}
|
|
@@ -1488,7 +1539,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1488
1539
|
usage: totalUsage || undefined,
|
|
1489
1540
|
stopReason,
|
|
1490
1541
|
...(finalReasoning ? { reasoning: finalReasoning } : {}),
|
|
1491
|
-
|
|
1542
|
+
turnMessages,
|
|
1492
1543
|
};
|
|
1493
1544
|
}
|
|
1494
1545
|
/**
|
|
@@ -1526,6 +1577,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1526
1577
|
context: {
|
|
1527
1578
|
cwd: this._cwd,
|
|
1528
1579
|
toolCallId,
|
|
1580
|
+
sessionId: this.config.sessionId,
|
|
1529
1581
|
signal: options.signal,
|
|
1530
1582
|
toolResponseStore: this.toolResponseStore,
|
|
1531
1583
|
discoveredTools: this.discoveredTools,
|
|
@@ -22,7 +22,7 @@ export class GeminiBackend extends BaseBackend {
|
|
|
22
22
|
const resultText = typeof block.content === 'string'
|
|
23
23
|
? block.content
|
|
24
24
|
: block.content.map(b => b.type === 'text' ? b.text : '[image]').join('\n');
|
|
25
|
-
return { text: `Function result: ${resultText}` };
|
|
25
|
+
return { text: `Function ${block.tool_use_id} ${block.is_error ? 'error' : 'result'}: ${resultText}` };
|
|
26
26
|
}
|
|
27
27
|
if (block.type === 'tool_use') {
|
|
28
28
|
// Convert tool_use to function call format for Gemini
|
|
@@ -36,7 +36,7 @@ export function buildOpenAIMessages(request) {
|
|
|
36
36
|
toolUses.push({ id: block.id, name: block.name, input: block.input });
|
|
37
37
|
}
|
|
38
38
|
else if (block.type === 'tool_result') {
|
|
39
|
-
toolResults.push({ tool_use_id: block.tool_use_id, content: block.content });
|
|
39
|
+
toolResults.push({ tool_use_id: block.tool_use_id, content: block.content, is_error: block.is_error });
|
|
40
40
|
}
|
|
41
41
|
else if (block.type === 'image') {
|
|
42
42
|
mediaParts.push({
|
|
@@ -80,7 +80,7 @@ export function buildOpenAIMessages(request) {
|
|
|
80
80
|
out.push({
|
|
81
81
|
role: 'tool',
|
|
82
82
|
tool_call_id: tr.tool_use_id,
|
|
83
|
-
content: resultText,
|
|
83
|
+
content: tr.is_error && !/^error\b/i.test(resultText) ? `Error: ${resultText}` : resultText,
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
// A tool_result merged with a later real user message: OpenAI still
|
|
@@ -179,7 +179,7 @@ export function buildResponsesInput(messages) {
|
|
|
179
179
|
input.push({
|
|
180
180
|
type: 'function_call_output',
|
|
181
181
|
call_id: block.tool_use_id,
|
|
182
|
-
output: resultText,
|
|
182
|
+
output: block.is_error && !/^error\b/i.test(resultText) ? `Error: ${resultText}` : resultText,
|
|
183
183
|
});
|
|
184
184
|
break;
|
|
185
185
|
}
|
|
@@ -116,7 +116,7 @@ const STATUS_LINE_USAGE = [
|
|
|
116
116
|
export const configCommand = {
|
|
117
117
|
name: 'config',
|
|
118
118
|
description: 'Manage configuration',
|
|
119
|
-
usage: '[show|reset|scope|local|global|key|provider|endpoint|openai-compat|auth-mode|model|effort|display|spinner|statusline|goal-max-turns|manifests-dir|context-progress-bar|workflows] [value]',
|
|
119
|
+
usage: '[show|reset|scope|local|global|key|provider|endpoint|openai-compat|auth-mode|model|max-tokens|effort|display|spinner|statusline|goal-max-turns|manifests-dir|context-progress-bar|workflows] [value]',
|
|
120
120
|
agentRunnable: true,
|
|
121
121
|
category: 'config',
|
|
122
122
|
handler: async (args, ctx) => {
|
|
@@ -139,6 +139,20 @@ export const configCommand = {
|
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
141
|
switch (subCmd) {
|
|
142
|
+
case 'max-tokens': {
|
|
143
|
+
const input = value.trim().toLowerCase();
|
|
144
|
+
if (input && input !== 'default' && (!/^[1-9]\d*$/.test(input) || !Number.isSafeInteger(Number(input)))) {
|
|
145
|
+
ctx.addMessage({ role: 'system', content: 'Max tokens must be a positive whole number, or default.' });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (input) {
|
|
149
|
+
ctx.config.set('maxTokens', input === 'default' ? undefined : Number(input));
|
|
150
|
+
await ctx.config.save();
|
|
151
|
+
ctx.reloadAgent();
|
|
152
|
+
}
|
|
153
|
+
ctx.addMessage({ role: 'system', content: `Output cap: ${ctx.config.get().maxTokens ?? 'provider default'}; source: ${ctx.config.getMaxTokensSource?.() ?? 'host configuration'}; config write scope: ${ctx.config.getConfigScope?.() ?? 'global'}. Use /config max-tokens <number|default>.` });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
142
156
|
case 'reset': {
|
|
143
157
|
if (rest.length > 0) {
|
|
144
158
|
ctx.addMessage({ role: 'system', content: 'Usage: /config reset' });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveExecutionSettings } from '../../config/execution.js';
|
|
1
2
|
import { accountsCommand } from './accounts.js';
|
|
2
3
|
import { authCommand } from './auth.js';
|
|
3
4
|
import { activeProfile, builtinProfileEmulation, listConfigProfiles } from '../../config/profiles.js';
|
|
@@ -209,6 +210,16 @@ export async function buildSettingsCategories(ctx) {
|
|
|
209
210
|
},
|
|
210
211
|
],
|
|
211
212
|
},
|
|
213
|
+
{
|
|
214
|
+
name: 'Execution',
|
|
215
|
+
settings: Object.entries(resolveExecutionSettings(config.execution)).map(([key, value]) => ({
|
|
216
|
+
name: `execution_${key}`, label: { shellPanePolicy: 'Shell pane creation', readConcurrency: 'Concurrent reads',
|
|
217
|
+
maxCallsPerResponse: 'Calls per response', recovery: 'Failure recovery', repeatedFailureLimit: 'Repeated failure threshold',
|
|
218
|
+
recoveryAttempts: 'Recovery attempts', interruptBackgroundJobs: 'Cancel background jobs on interrupt', maxBackgroundJobs: 'Background command limit' }[key],
|
|
219
|
+
value: String(value), type: typeof value === 'number' ? 'text' : 'select',
|
|
220
|
+
options: (key === 'shellPanePolicy' ? ['on_request', 'auto'] : key === 'recovery' ? ['automatic', 'pause'] : typeof value === 'boolean' ? ['false', 'true'] : []).map(option => ({ value: option, label: option })),
|
|
221
|
+
})),
|
|
222
|
+
},
|
|
212
223
|
{
|
|
213
224
|
name: 'Accounts',
|
|
214
225
|
settings: [{
|
|
@@ -1025,6 +1036,18 @@ function openCustomPaneManager(ctx, reopen) {
|
|
|
1025
1036
|
showList();
|
|
1026
1037
|
}
|
|
1027
1038
|
function applySettingChange(ctx, setting, value) {
|
|
1039
|
+
if (setting.startsWith('execution_')) {
|
|
1040
|
+
const key = setting.slice('execution_'.length);
|
|
1041
|
+
const current = resolveExecutionSettings(ctx.config.get().execution);
|
|
1042
|
+
if (!(key in current))
|
|
1043
|
+
throw new Error(`Unknown execution setting: ${key}`);
|
|
1044
|
+
const parsed = typeof current[key] === 'number' ? Number(value) : typeof current[key] === 'boolean' ? value === 'true' : value;
|
|
1045
|
+
const next = { ...current, [key]: parsed };
|
|
1046
|
+
if (String(resolveExecutionSettings(next)[key]) !== value)
|
|
1047
|
+
throw new Error(`Invalid value for ${key}: ${value}`);
|
|
1048
|
+
ctx.config.set('execution', next);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1028
1051
|
if (setting.startsWith('toolPermission_')) {
|
|
1029
1052
|
const capability = setting.slice('toolPermission_'.length);
|
|
1030
1053
|
if (!['file_read', 'file_write', 'shell_exec', 'network', 'screen_capture', 'computer_control', 'ml_inference'].includes(capability)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeExecutionContext } from '../runtime/execution_context.js';
|
|
1
2
|
import { resolveAuthMode } from '../../config.js';
|
|
2
3
|
import { formatRuntimeSummary, getRuntimeInfo } from '../../runtime_info.js';
|
|
3
4
|
import { getWorkflowRegistry } from '../../workflows/registry_handle.js';
|
|
@@ -44,10 +45,12 @@ export const statusCommand = {
|
|
|
44
45
|
catch (error) {
|
|
45
46
|
routeError = error instanceof Error ? error.message : String(error);
|
|
46
47
|
}
|
|
48
|
+
const execution = ctx.getExecutionContext?.();
|
|
47
49
|
ctx.addMessage({
|
|
48
50
|
role: 'system',
|
|
49
51
|
content: [
|
|
50
52
|
'Status:',
|
|
53
|
+
...(execution ? [describeExecutionContext(execution)] : []),
|
|
51
54
|
` API Key: ${hasKey ? '✓ configured' : '✗ not set (use /config key <key>)'}`,
|
|
52
55
|
` Platform Auth: ${isAuthenticated ? '✓ configured' : '✗ not set'}`,
|
|
53
56
|
` Provider: ${provider}`,
|
|
@@ -14,6 +14,7 @@ export interface ConfigController {
|
|
|
14
14
|
set: <K extends keyof ZTCConfig>(key: K, value: ZTCConfig[K]) => void;
|
|
15
15
|
setGlobal?: <K extends keyof ZTCConfig>(key: K, value: ZTCConfig[K]) => void;
|
|
16
16
|
getConfigScope?: () => ZTCConfigScope;
|
|
17
|
+
getMaxTokensSource?: () => string;
|
|
17
18
|
setConfigScope?: (scope: ZTCConfigScope) => void;
|
|
18
19
|
clearSessionOverrides?: () => void;
|
|
19
20
|
withConfigScope?: <T>(scope: ZTCConfigScope, fn: () => T | Promise<T>) => Promise<T>;
|
|
@@ -304,6 +305,7 @@ export interface CommandContext {
|
|
|
304
305
|
exit: () => void;
|
|
305
306
|
/** Current session ID. Used by /rename and other session-scoped commands. */
|
|
306
307
|
getSessionId?: () => string;
|
|
308
|
+
getExecutionContext?: () => import('../runtime/execution_context.js').ExecutionContext | undefined;
|
|
307
309
|
/** Current user-set session name (live, post-rename). Undefined if not named. */
|
|
308
310
|
getSessionName?: () => string | undefined;
|
|
309
311
|
/**
|
|
@@ -20,10 +20,10 @@ export function incompleteModelResponseMessage(stopReason, maxTokens, outputToke
|
|
|
20
20
|
if (limited || exhausted) {
|
|
21
21
|
if (maxTokens === undefined) {
|
|
22
22
|
return 'Model reached the provider\'s output limit (including reasoning). '
|
|
23
|
-
+ '
|
|
23
|
+
+ 'Use /config max-tokens <number> within the model\'s limit or lower reasoning effort, then retry; completed tool work is retained and the incomplete response was not accepted.';
|
|
24
24
|
}
|
|
25
25
|
return `Model reached the output limit (maxTokens: ${maxTokens}, including reasoning). `
|
|
26
|
-
+ '
|
|
26
|
+
+ 'Set /config max-tokens <number> or /config max-tokens default, then retry. Completed tool work is retained; the incomplete response was not accepted.';
|
|
27
27
|
}
|
|
28
28
|
return INCOMPLETE_MODEL_RESPONSE_MESSAGE;
|
|
29
29
|
}
|
package/dist/agent/factory.js
CHANGED
|
@@ -677,7 +677,7 @@ export function createAgentFromConfig(options) {
|
|
|
677
677
|
emulationMappingPrompt,
|
|
678
678
|
skillPrompt: effectiveSkillPrompt,
|
|
679
679
|
modePrompt,
|
|
680
|
-
muxEnabled:
|
|
680
|
+
muxEnabled: filteredToolNames.has('create_terminal_pane'),
|
|
681
681
|
agentsContext: projectContext,
|
|
682
682
|
runtimeContext,
|
|
683
683
|
askUserAvailable: filteredToolNames.has('ask_user'),
|
|
@@ -715,6 +715,12 @@ export function createAgentFromConfig(options) {
|
|
|
715
715
|
// Hosted runtimes never pin the task header; other callers may opt in.
|
|
716
716
|
pinTaskHeader: hostRuntime ? false : options.pinTaskHeader,
|
|
717
717
|
tools: filteredTools,
|
|
718
|
+
execution: options.config.execution,
|
|
719
|
+
sessionId: options.sessionId,
|
|
720
|
+
runtimeFacts: { mode, mux: Boolean(options.muxMode || effectivePaneMode),
|
|
721
|
+
configScope: configStore.getConfigScope(),
|
|
722
|
+
outputLimitSource: options.config.maxTokens === configStore.get().maxTokens ? configStore.getMaxTokensSource() : 'host configuration',
|
|
723
|
+
},
|
|
718
724
|
// Agent-domain tools (ZTC_AGENT_DOMAIN) are the host's curated surface — ship
|
|
719
725
|
// their schemas every turn instead of hiding them behind the deferred-tool
|
|
720
726
|
// search corpus (which the mode allowlist can also strip).
|