tinker-agent 1.8.0 → 1.10.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/CHANGELOG.md +41 -1
- package/README.md +65 -6
- package/package.json +1 -1
- package/src/agent/loop.ts +13 -0
- package/src/agent/runtime-session.ts +165 -0
- package/src/agent/session-ledger.ts +20 -3
- package/src/cli/config.ts +11 -2
- package/src/cli/model-profiles.ts +58 -0
- package/src/cli/public-config-contract.ts +73 -7
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/runner-dependencies.ts +35 -10
- package/src/cli/tui-memory.ts +4 -0
- package/src/cli/tui-runner.tsx +8 -1
- package/src/context/context-automation-policy.ts +22 -21
- package/src/context/context-manager.ts +91 -15
- package/src/context/context-policy.ts +0 -2
- package/src/context/context-swap-renderer.ts +1 -1
- package/src/context/prefix-retirement-planner.ts +58 -8
- package/src/context/recall-retirement-contract.ts +5 -4
- package/src/context/swap-planner.ts +33 -27
- package/src/events/stdout-event-printer.ts +17 -0
- package/src/model/fake-model-client.ts +26 -16
- package/src/model/model-api.ts +12 -0
- package/src/model/model-client.ts +9 -1
- package/src/model/moonshot-input-token-estimator.ts +5 -1
- package/src/model/openai-chat-mapping.ts +2 -24
- package/src/model/openai-chat-model-client.ts +18 -294
- package/src/model/openai-image-mapping.ts +20 -0
- package/src/model/openai-model-utils.ts +304 -0
- package/src/model/openai-responses-mapping.ts +532 -0
- package/src/model/openai-responses-model-client.ts +295 -0
- package/src/model/openai-responses-stream.ts +96 -0
- package/src/model/openai-responses-token-estimator.ts +155 -0
- package/src/model/reasoning-effort.ts +60 -0
- package/src/observation/observation-builder.ts +7 -0
- package/src/session/session-catalog.ts +2 -2
- package/src/session/session-history-reader.ts +6 -1
- package/src/session/session-schema.ts +268 -4
- package/src/session/session-store.ts +106 -26
- package/src/skills/skill-context.ts +2 -2
- package/src/tools/bounded-output-preview.ts +276 -0
- package/src/tools/recall.ts +67 -36
- package/src/tools/registry.ts +9 -2
- package/src/tools/task-output-snapshot.ts +6 -22
- package/src/tools/task-output.ts +23 -27
- package/src/tools/types.ts +19 -0
- package/src/tools/update-plan.ts +166 -0
- package/src/tui/app.tsx +82 -5
- package/src/tui/components/plan-view.tsx +43 -0
- package/src/tui/components/prompt-input.tsx +9 -1
- package/src/tui/components/timeline.tsx +7 -0
- package/src/tui/event-store.ts +28 -2
- package/src/tui/slash-commands.ts +20 -0
- package/src/tui/tui-session-controller.ts +7 -0
package/src/tools/types.ts
CHANGED
|
@@ -144,6 +144,24 @@ export type TaskListRawResult = {
|
|
|
144
144
|
error?: string;
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
+
export type PlanStepStatus = "pending" | "in_progress" | "completed";
|
|
148
|
+
|
|
149
|
+
export type PlanStep = {
|
|
150
|
+
step: string;
|
|
151
|
+
status: PlanStepStatus;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export type UpdatePlanRawResult =
|
|
155
|
+
| {
|
|
156
|
+
ok: true;
|
|
157
|
+
explanation?: string;
|
|
158
|
+
plan: PlanStep[];
|
|
159
|
+
}
|
|
160
|
+
| {
|
|
161
|
+
ok: false;
|
|
162
|
+
error: string;
|
|
163
|
+
};
|
|
164
|
+
|
|
147
165
|
export type TaskOutputRawResult = {
|
|
148
166
|
ok: boolean;
|
|
149
167
|
taskId: string;
|
|
@@ -355,6 +373,7 @@ export type ToolRawResultByKind = {
|
|
|
355
373
|
glob: GlobRawResult;
|
|
356
374
|
grep: GrepRawResult;
|
|
357
375
|
bash: BashRawResult;
|
|
376
|
+
update_plan: UpdatePlanRawResult;
|
|
358
377
|
task_list: TaskListRawResult;
|
|
359
378
|
task_output: TaskOutputRawResult;
|
|
360
379
|
task_input: TaskInputRawResult;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
2
|
+
import {
|
|
3
|
+
defineToolExecutor,
|
|
4
|
+
type PlanStep,
|
|
5
|
+
type ToolExecutionContext,
|
|
6
|
+
type ToolExecutor,
|
|
7
|
+
type UpdatePlanRawResult,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
const MAX_PLAN_ITEMS = 12;
|
|
11
|
+
const MAX_STEP_LENGTH = 200;
|
|
12
|
+
const MAX_EXPLANATION_LENGTH = 500;
|
|
13
|
+
const STATUSES = new Set(["pending", "in_progress", "completed"]);
|
|
14
|
+
|
|
15
|
+
export function createUpdatePlanToolExecutor(): ToolExecutor {
|
|
16
|
+
return defineToolExecutor("update_plan", {
|
|
17
|
+
definition: {
|
|
18
|
+
name: "UpdatePlan",
|
|
19
|
+
description:
|
|
20
|
+
"Replace the current task plan with a complete ordered list of steps and their progress. Use an optional explanation when revising the approach. At most one step may be in_progress.",
|
|
21
|
+
parameters: {
|
|
22
|
+
type: "object",
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
properties: {
|
|
25
|
+
explanation: {
|
|
26
|
+
type: "string",
|
|
27
|
+
maxLength: MAX_EXPLANATION_LENGTH,
|
|
28
|
+
description: "Optional explanation for this plan update.",
|
|
29
|
+
},
|
|
30
|
+
plan: {
|
|
31
|
+
type: "array",
|
|
32
|
+
maxItems: MAX_PLAN_ITEMS,
|
|
33
|
+
description: "The complete ordered task plan.",
|
|
34
|
+
items: {
|
|
35
|
+
type: "object",
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
properties: {
|
|
38
|
+
step: {
|
|
39
|
+
type: "string",
|
|
40
|
+
minLength: 1,
|
|
41
|
+
maxLength: MAX_STEP_LENGTH,
|
|
42
|
+
description: "Task step text.",
|
|
43
|
+
},
|
|
44
|
+
status: {
|
|
45
|
+
type: "string",
|
|
46
|
+
enum: ["pending", "in_progress", "completed"],
|
|
47
|
+
description: "Step status.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ["step", "status"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ["plan"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
async execute(
|
|
58
|
+
args,
|
|
59
|
+
_call,
|
|
60
|
+
context: ToolExecutionContext,
|
|
61
|
+
): Promise<UpdatePlanRawResult> {
|
|
62
|
+
throwIfTurnCancelled(context.signal);
|
|
63
|
+
const parsed = parseUpdatePlanArgs(args);
|
|
64
|
+
if (!parsed.ok) {
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
throwIfTurnCancelled(context.signal);
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
...(parsed.explanation === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { explanation: parsed.explanation }),
|
|
73
|
+
plan: parsed.plan,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type ParsedUpdatePlanArgs =
|
|
80
|
+
| { ok: true; explanation?: string; plan: PlanStep[] }
|
|
81
|
+
| { ok: false; error: string };
|
|
82
|
+
|
|
83
|
+
function parseUpdatePlanArgs(args: unknown): ParsedUpdatePlanArgs {
|
|
84
|
+
if (!isRecord(args)) {
|
|
85
|
+
return failure("UpdatePlan arguments must be an object.");
|
|
86
|
+
}
|
|
87
|
+
const unexpected = Object.keys(args).find(
|
|
88
|
+
(key) => key !== "explanation" && key !== "plan",
|
|
89
|
+
);
|
|
90
|
+
if (unexpected !== undefined) {
|
|
91
|
+
return failure(`UpdatePlan received unexpected argument: ${unexpected}.`);
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(args.plan)) {
|
|
94
|
+
return failure("UpdatePlan plan must be an array.");
|
|
95
|
+
}
|
|
96
|
+
if (args.plan.length > MAX_PLAN_ITEMS) {
|
|
97
|
+
return failure(`UpdatePlan accepts at most ${MAX_PLAN_ITEMS} steps.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let explanation: string | undefined;
|
|
101
|
+
if (args.explanation !== undefined) {
|
|
102
|
+
if (typeof args.explanation !== "string") {
|
|
103
|
+
return failure("UpdatePlan explanation must be a string.");
|
|
104
|
+
}
|
|
105
|
+
explanation = args.explanation.trim();
|
|
106
|
+
if (explanation.length > MAX_EXPLANATION_LENGTH) {
|
|
107
|
+
return failure(
|
|
108
|
+
`UpdatePlan explanation must be at most ${MAX_EXPLANATION_LENGTH} characters.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (explanation === "") {
|
|
112
|
+
explanation = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const plan: PlanStep[] = [];
|
|
117
|
+
let inProgressCount = 0;
|
|
118
|
+
for (const [index, item] of args.plan.entries()) {
|
|
119
|
+
if (!isRecord(item)) {
|
|
120
|
+
return failure(`UpdatePlan plan[${index}] must be an object.`);
|
|
121
|
+
}
|
|
122
|
+
const unexpectedItemKey = Object.keys(item).find(
|
|
123
|
+
(key) => key !== "step" && key !== "status",
|
|
124
|
+
);
|
|
125
|
+
if (unexpectedItemKey !== undefined) {
|
|
126
|
+
return failure(
|
|
127
|
+
`UpdatePlan plan[${index}] received unexpected field: ${unexpectedItemKey}.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (typeof item.step !== "string" || item.step.trim() === "") {
|
|
131
|
+
return failure(`UpdatePlan plan[${index}].step must be a non-empty string.`);
|
|
132
|
+
}
|
|
133
|
+
const step = item.step.trim();
|
|
134
|
+
if (step.length > MAX_STEP_LENGTH) {
|
|
135
|
+
return failure(
|
|
136
|
+
`UpdatePlan plan[${index}].step must be at most ${MAX_STEP_LENGTH} characters.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (typeof item.status !== "string" || !STATUSES.has(item.status)) {
|
|
140
|
+
return failure(
|
|
141
|
+
`UpdatePlan plan[${index}].status must be pending, in_progress, or completed.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (item.status === "in_progress") {
|
|
145
|
+
inProgressCount += 1;
|
|
146
|
+
}
|
|
147
|
+
plan.push({ step, status: item.status as PlanStep["status"] });
|
|
148
|
+
}
|
|
149
|
+
if (inProgressCount > 1) {
|
|
150
|
+
return failure("UpdatePlan allows at most one in_progress step.");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
...(explanation === undefined ? {} : { explanation }),
|
|
156
|
+
plan,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function failure(error: string): ParsedUpdatePlanArgs {
|
|
161
|
+
return { ok: false, error };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
165
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
166
|
+
}
|
package/src/tui/app.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "../context/context-manager";
|
|
18
18
|
import { ContextBudgetExceededError } from "../model/model-request-preflight";
|
|
19
19
|
import { ModelRequestMediaAggregateError } from "../model/model-client";
|
|
20
|
+
import type { ReasoningEffortSnapshot } from "../model/reasoning-effort";
|
|
20
21
|
import type { SessionId } from "../ids/runtime-id";
|
|
21
22
|
import { readLastAssistantResponse } from "../session/session-last-response-reader";
|
|
22
23
|
import type { PromptHistory } from "./prompt-history";
|
|
@@ -180,15 +181,43 @@ export function App(props: AppProps) {
|
|
|
180
181
|
state.activeTurn === undefined &&
|
|
181
182
|
props.profiles !== undefined &&
|
|
182
183
|
props.profiles.profiles.size > 1;
|
|
184
|
+
const reasoningEffort = binding.reasoningEffort?.();
|
|
185
|
+
const hasReasoningEffort = reasoningEffort !== undefined;
|
|
183
186
|
const activeResumePicker =
|
|
184
187
|
resumePicker?.ownerSessionId === binding.sessionId ? resumePicker : undefined;
|
|
185
188
|
|
|
186
|
-
const builtInCommands =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
const builtInCommands = SLASH_COMMANDS.filter(
|
|
190
|
+
(command) =>
|
|
191
|
+
(command.name !== "model" || canSwitchModel) &&
|
|
192
|
+
(command.name !== "reasoning" || hasReasoningEffort),
|
|
193
|
+
);
|
|
189
194
|
const availableCommands = [...builtInCommands, ...(props.projectSlashCommands ?? [])];
|
|
190
195
|
|
|
191
196
|
const profileList = props.profiles ? [...props.profiles.profiles.values()] : [];
|
|
197
|
+
const cycleReasoningEffort = () => {
|
|
198
|
+
const current = binding.reasoningEffort?.();
|
|
199
|
+
if (current === undefined) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const currentIndex = current.supportedEfforts.indexOf(current.effort);
|
|
203
|
+
const nextIndex =
|
|
204
|
+
currentIndex < 0 ? 0 : (currentIndex + 1) % current.supportedEfforts.length;
|
|
205
|
+
const nextEffort = current.supportedEfforts[nextIndex];
|
|
206
|
+
if (nextEffort === undefined) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const updated = binding.setReasoningEffort?.(nextEffort);
|
|
211
|
+
if (updated === undefined) {
|
|
212
|
+
throw new Error("Reasoning effort control is unavailable.");
|
|
213
|
+
}
|
|
214
|
+
setNotice(
|
|
215
|
+
`Reasoning effort cycled from ${JSON.stringify(current.effort)} to ${JSON.stringify(updated.effort)} (Ctrl+R).`,
|
|
216
|
+
);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
setNotice(errorMessage(error));
|
|
219
|
+
}
|
|
220
|
+
};
|
|
192
221
|
|
|
193
222
|
useEffect(() => {
|
|
194
223
|
if (readGitBranch === undefined) {
|
|
@@ -660,6 +689,42 @@ export function App(props: AppProps) {
|
|
|
660
689
|
exit();
|
|
661
690
|
return true;
|
|
662
691
|
}
|
|
692
|
+
if (
|
|
693
|
+
command.type === "reasoning_status" ||
|
|
694
|
+
command.type === "reasoning_reset" ||
|
|
695
|
+
command.type === "reasoning_set"
|
|
696
|
+
) {
|
|
697
|
+
const current = binding.reasoningEffort?.();
|
|
698
|
+
if (current === undefined) {
|
|
699
|
+
setNotice("Current model profile does not configure reasoning effort.");
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
if (command.type === "reasoning_status") {
|
|
704
|
+
setNotice(formatReasoningEffortStatus(current));
|
|
705
|
+
} else if (command.type === "reasoning_reset") {
|
|
706
|
+
const reset = binding.resetReasoningEffort?.();
|
|
707
|
+
if (reset === undefined) {
|
|
708
|
+
throw new Error("Reasoning effort control is unavailable.");
|
|
709
|
+
}
|
|
710
|
+
setNotice(
|
|
711
|
+
`Reasoning effort reset to profile default ${JSON.stringify(reset.defaultEffort)}.`,
|
|
712
|
+
);
|
|
713
|
+
} else {
|
|
714
|
+
const updated = binding.setReasoningEffort?.(command.effort);
|
|
715
|
+
if (updated === undefined) {
|
|
716
|
+
throw new Error("Reasoning effort control is unavailable.");
|
|
717
|
+
}
|
|
718
|
+
setNotice(
|
|
719
|
+
`Reasoning effort set to ${JSON.stringify(updated.effort)} for this session runtime (profile default: ${JSON.stringify(updated.defaultEffort)}).`,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
setNotice(errorMessage(error));
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
663
728
|
if (command.type === "model" || command.type === "model_switch") {
|
|
664
729
|
if (!canSwitchModel) {
|
|
665
730
|
setNotice(
|
|
@@ -821,6 +886,7 @@ export function App(props: AppProps) {
|
|
|
821
886
|
) : (
|
|
822
887
|
<PromptInput
|
|
823
888
|
modelName={binding.modelName}
|
|
889
|
+
reasoningEffort={reasoningEffort?.effort}
|
|
824
890
|
workspaceRoot={binding.workspaceRoot}
|
|
825
891
|
gitBranch={gitBranch}
|
|
826
892
|
contextUsage={state.contextUsage}
|
|
@@ -835,6 +901,9 @@ export function App(props: AppProps) {
|
|
|
835
901
|
fileLister={props.fileLister}
|
|
836
902
|
importImage={binding.importImage}
|
|
837
903
|
verifyImageAssets={binding.verifyImageAssets}
|
|
904
|
+
onCycleReasoningEffort={
|
|
905
|
+
hasReasoningEffort ? cycleReasoningEffort : undefined
|
|
906
|
+
}
|
|
838
907
|
onSubmit={onSubmit}
|
|
839
908
|
onMaintenance={onMaintenance}
|
|
840
909
|
placeholder='Enter a coding request, or "/" for commands'
|
|
@@ -850,6 +919,14 @@ export function App(props: AppProps) {
|
|
|
850
919
|
);
|
|
851
920
|
}
|
|
852
921
|
|
|
922
|
+
function formatReasoningEffortStatus(snapshot: ReasoningEffortSnapshot): string {
|
|
923
|
+
const source =
|
|
924
|
+
snapshot.source === "profile_default"
|
|
925
|
+
? "profile default"
|
|
926
|
+
: `session override; profile default: ${snapshot.defaultEffort}`;
|
|
927
|
+
return `Reasoning effort: ${snapshot.effort} (${source}). Available: ${snapshot.supportedEfforts.join(", ")}.`;
|
|
928
|
+
}
|
|
929
|
+
|
|
853
930
|
function errorMessage(error: unknown): string {
|
|
854
931
|
return error instanceof Error ? error.message : String(error);
|
|
855
932
|
}
|
|
@@ -947,7 +1024,7 @@ export function formatContextRetirementNotice(result: ContextRetirementResult):
|
|
|
947
1024
|
const before = result.guardedTokensBefore.toLocaleString("en-US");
|
|
948
1025
|
const after = result.guardedTokensAfter.toLocaleString("en-US");
|
|
949
1026
|
if (result.outcome === "retirement_floor") {
|
|
950
|
-
return `Context prefix retired: revision ${result.previousRevisionNumber} -> ${result.revisionNumber}, ${result.retiredTurnCount} turns removed from the active request, ${before} -> ${after} estimated tokens; target ${result.targetTokens.toLocaleString("en-US")} was not reached. Run /compact first when retained tool output is still eligible. Older history remains available through
|
|
1027
|
+
return `Context prefix retired: revision ${result.previousRevisionNumber} -> ${result.revisionNumber}, ${result.retiredTurnCount} turns removed from the active request, ${before} -> ${after} estimated tokens; target ${result.targetTokens.toLocaleString("en-US")} was not reached. Run /compact first when retained tool output is still eligible. Older history remains available through RecallSearch and RecallGet.`;
|
|
951
1028
|
}
|
|
952
1029
|
const reduction =
|
|
953
1030
|
result.guardedTokensBefore === 0
|
|
@@ -957,7 +1034,7 @@ export function formatContextRetirementNotice(result: ContextRetirementResult):
|
|
|
957
1034
|
result.guardedTokensBefore) *
|
|
958
1035
|
100
|
|
959
1036
|
).toFixed(1);
|
|
960
|
-
return `Context prefix retired: revision ${result.previousRevisionNumber} -> ${result.revisionNumber}, ${result.retiredTurnCount} turns removed from the active request, ${before} -> ${after} estimated tokens (-${reduction}%). Older history remains available through
|
|
1037
|
+
return `Context prefix retired: revision ${result.previousRevisionNumber} -> ${result.revisionNumber}, ${result.retiredTurnCount} turns removed from the active request, ${before} -> ${after} estimated tokens (-${reduction}%). Older history remains available through RecallSearch and RecallGet.`;
|
|
961
1038
|
}
|
|
962
1039
|
|
|
963
1040
|
export function formatContextRetirementFailureNotice(error: unknown): string {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { TimelineItem } from "../event-store";
|
|
3
|
+
|
|
4
|
+
export function PlanView(props: { plan: NonNullable<TimelineItem["plan"]> }) {
|
|
5
|
+
return (
|
|
6
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
7
|
+
{props.plan.explanation === undefined ? null : (
|
|
8
|
+
<Text dimColor italic>
|
|
9
|
+
{props.plan.explanation}
|
|
10
|
+
</Text>
|
|
11
|
+
)}
|
|
12
|
+
{props.plan.steps.length === 0 ? (
|
|
13
|
+
<Text dimColor italic>
|
|
14
|
+
(no steps)
|
|
15
|
+
</Text>
|
|
16
|
+
) : (
|
|
17
|
+
props.plan.steps.map((step, index) => (
|
|
18
|
+
<PlanStepRow key={`${index}:${step.step}`} step={step} />
|
|
19
|
+
))
|
|
20
|
+
)}
|
|
21
|
+
</Box>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function PlanStepRow(props: {
|
|
26
|
+
step: NonNullable<TimelineItem["plan"]>["steps"][number];
|
|
27
|
+
}) {
|
|
28
|
+
if (props.step.status === "completed") {
|
|
29
|
+
return (
|
|
30
|
+
<Text dimColor strikethrough>
|
|
31
|
+
✓ {props.step.step}
|
|
32
|
+
</Text>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (props.step.status === "in_progress") {
|
|
36
|
+
return (
|
|
37
|
+
<Text color="cyan" bold>
|
|
38
|
+
→ {props.step.step}
|
|
39
|
+
</Text>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return <Text dimColor>• {props.step.step}</Text>;
|
|
43
|
+
}
|
|
@@ -60,6 +60,7 @@ export type PromptSubmissionOutcome =
|
|
|
60
60
|
|
|
61
61
|
export type PromptInputProps = {
|
|
62
62
|
modelName: string;
|
|
63
|
+
reasoningEffort?: string;
|
|
63
64
|
workspaceRoot: string;
|
|
64
65
|
gitBranch?: string;
|
|
65
66
|
contextUsage?: ContextUsageSnapshot;
|
|
@@ -81,6 +82,7 @@ export type PromptInputProps = {
|
|
|
81
82
|
assets: readonly ImageAssetRef[],
|
|
82
83
|
signal: AbortSignal,
|
|
83
84
|
) => Promise<void>;
|
|
85
|
+
onCycleReasoningEffort?: () => void;
|
|
84
86
|
onSubmit: (
|
|
85
87
|
submission: PromptSubmission,
|
|
86
88
|
signal: AbortSignal,
|
|
@@ -605,6 +607,10 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
605
607
|
}
|
|
606
608
|
return;
|
|
607
609
|
}
|
|
610
|
+
if (key.ctrl && input === "r") {
|
|
611
|
+
props.onCycleReasoningEffort?.();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
608
614
|
const selectedFile = fileMatches[selectedIndex];
|
|
609
615
|
const selectedCommand = suggestions[selectedIndex];
|
|
610
616
|
if (key.return) {
|
|
@@ -736,7 +742,9 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
736
742
|
{showSuggestions ? null : (
|
|
737
743
|
<Box>
|
|
738
744
|
<Text dimColor>
|
|
739
|
-
{props.modelName}
|
|
745
|
+
{props.modelName}
|
|
746
|
+
{props.reasoningEffort === undefined ? null : ` ${props.reasoningEffort}`} ·{" "}
|
|
747
|
+
{formatWorkspacePath(props.workspaceRoot)}
|
|
740
748
|
{props.gitBranch === undefined ? null : ` · ${props.gitBranch}`}
|
|
741
749
|
{state.phase.kind === "idle" ? null : ` · ${phaseLabel(state.phase)}`}
|
|
742
750
|
</Text>
|
|
@@ -5,6 +5,7 @@ import type { AssistantStreamSectionItem } from "../tui-projection-store";
|
|
|
5
5
|
import { AssistantMarkdown } from "./assistant-markdown";
|
|
6
6
|
import { BashResultView } from "./bash-result-view";
|
|
7
7
|
import { DiffView } from "./diff-view";
|
|
8
|
+
import { PlanView } from "./plan-view";
|
|
8
9
|
|
|
9
10
|
export type TimelineProps = {
|
|
10
11
|
items: readonly TimelineItem[];
|
|
@@ -42,6 +43,7 @@ export function TimelineRow(props: { item: TimelineItem }) {
|
|
|
42
43
|
)}
|
|
43
44
|
{renderItemBash(item)}
|
|
44
45
|
{renderItemDiff(item)}
|
|
46
|
+
{renderItemPlan(item)}
|
|
45
47
|
</Fragment>
|
|
46
48
|
);
|
|
47
49
|
}
|
|
@@ -51,6 +53,7 @@ export function TimelineRow(props: { item: TimelineItem }) {
|
|
|
51
53
|
<Text color={colorForStatus(item.status)}>{formatTimelineItem(item)}</Text>
|
|
52
54
|
{renderItemBash(item)}
|
|
53
55
|
{renderItemDiff(item)}
|
|
56
|
+
{renderItemPlan(item)}
|
|
54
57
|
</Fragment>
|
|
55
58
|
);
|
|
56
59
|
}
|
|
@@ -105,6 +108,10 @@ function renderItemBash(item: TimelineItem) {
|
|
|
105
108
|
return <BashResultView detail={item.bash} />;
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
function renderItemPlan(item: TimelineItem) {
|
|
112
|
+
return item.plan === undefined ? null : <PlanView plan={item.plan} />;
|
|
113
|
+
}
|
|
114
|
+
|
|
108
115
|
function formatTimelineItem(item: TimelineItem): string {
|
|
109
116
|
if (item.status === "text") {
|
|
110
117
|
return item.text;
|
package/src/tui/event-store.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from "../model/model-context-profile";
|
|
12
12
|
import type { ShellTaskSnapshot, ShellTaskStatus } from "../tools/bash-task";
|
|
13
13
|
import { countPatchChanges } from "../tools/file-diff";
|
|
14
|
-
import type { DiffHunk, ToolRawResult } from "../tools/types";
|
|
14
|
+
import type { DiffHunk, PlanStep, ToolRawResult } from "../tools/types";
|
|
15
15
|
import {
|
|
16
16
|
defaultTuiProjectionPolicy,
|
|
17
17
|
type TuiProjectionPolicy,
|
|
@@ -33,6 +33,10 @@ export type TimelineItem = {
|
|
|
33
33
|
diffTruncated?: boolean;
|
|
34
34
|
bash?: BashDisplayDetail;
|
|
35
35
|
userPrompt?: UserPromptProjection;
|
|
36
|
+
plan?: {
|
|
37
|
+
explanation?: string;
|
|
38
|
+
steps: readonly PlanStep[];
|
|
39
|
+
};
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
export type TuiTurnProjection = {
|
|
@@ -747,11 +751,12 @@ export function toolCallStartedProjection(input: {
|
|
|
747
751
|
export function toolRawResultProjection(
|
|
748
752
|
input: { name: string; args: unknown },
|
|
749
753
|
raw: ToolRawResult,
|
|
750
|
-
): Pick<TimelineItem, "text" | "bash" | "diff" | "diffTruncated"> {
|
|
754
|
+
): Pick<TimelineItem, "text" | "bash" | "diff" | "diffTruncated" | "plan"> {
|
|
751
755
|
return {
|
|
752
756
|
text: toolRawResultSummary(input.name, input.args, raw),
|
|
753
757
|
...toolRawResultDiff(raw),
|
|
754
758
|
...toolRawResultBashDetail(raw),
|
|
759
|
+
...toolRawResultPlanDetail(raw),
|
|
755
760
|
};
|
|
756
761
|
}
|
|
757
762
|
|
|
@@ -888,6 +893,13 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
888
893
|
: `${base} -> running ${raw.outputFilePath}`;
|
|
889
894
|
}
|
|
890
895
|
return raw.exitCode === undefined ? base : `${base} -> exit ${raw.exitCode}`;
|
|
896
|
+
case "update_plan": {
|
|
897
|
+
if (!raw.ok) {
|
|
898
|
+
return base;
|
|
899
|
+
}
|
|
900
|
+
const completed = raw.plan.filter((step) => step.status === "completed").length;
|
|
901
|
+
return `${base} -> ${completed}/${raw.plan.length} completed`;
|
|
902
|
+
}
|
|
891
903
|
case "mcp":
|
|
892
904
|
case "generic":
|
|
893
905
|
return base;
|
|
@@ -929,6 +941,7 @@ function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash">
|
|
|
929
941
|
case "delete":
|
|
930
942
|
case "glob":
|
|
931
943
|
case "grep":
|
|
944
|
+
case "update_plan":
|
|
932
945
|
case "task_list":
|
|
933
946
|
case "task_stop":
|
|
934
947
|
case "web_search":
|
|
@@ -964,6 +977,7 @@ function toolRawResultDiff(
|
|
|
964
977
|
case "glob":
|
|
965
978
|
case "grep":
|
|
966
979
|
case "bash":
|
|
980
|
+
case "update_plan":
|
|
967
981
|
case "task_list":
|
|
968
982
|
case "task_output":
|
|
969
983
|
case "task_input":
|
|
@@ -981,6 +995,18 @@ function toolRawResultDiff(
|
|
|
981
995
|
}
|
|
982
996
|
}
|
|
983
997
|
|
|
998
|
+
function toolRawResultPlanDetail(raw: ToolRawResult): Pick<TimelineItem, "plan"> {
|
|
999
|
+
if (raw.kind !== "update_plan" || !raw.ok) {
|
|
1000
|
+
return {};
|
|
1001
|
+
}
|
|
1002
|
+
return {
|
|
1003
|
+
plan: {
|
|
1004
|
+
...(raw.explanation === undefined ? {} : { explanation: raw.explanation }),
|
|
1005
|
+
steps: raw.plan.map((step) => ({ ...step })),
|
|
1006
|
+
},
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
984
1010
|
function upsertBackgroundTask(
|
|
985
1011
|
tasks: ShellTaskSnapshot[],
|
|
986
1012
|
task: ShellTaskSnapshot,
|
|
@@ -65,6 +65,11 @@ export const SLASH_COMMANDS: readonly BuiltInSlashCommand[] = [
|
|
|
65
65
|
usage: "/model [profile-name]",
|
|
66
66
|
description: "Switch model profile (new session)",
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
name: "reasoning",
|
|
70
|
+
usage: "/reasoning [effort|reset]",
|
|
71
|
+
description: "Show or change reasoning effort for this session runtime",
|
|
72
|
+
},
|
|
68
73
|
{
|
|
69
74
|
name: "resume",
|
|
70
75
|
usage: "/resume [session-id]",
|
|
@@ -95,6 +100,9 @@ export type ParsedSlashCommand =
|
|
|
95
100
|
| { type: "quit" }
|
|
96
101
|
| { type: "model" }
|
|
97
102
|
| { type: "model_switch"; profileName: string }
|
|
103
|
+
| { type: "reasoning_status" }
|
|
104
|
+
| { type: "reasoning_reset" }
|
|
105
|
+
| { type: "reasoning_set"; effort: string }
|
|
98
106
|
| { type: "resume_list" }
|
|
99
107
|
| { type: "resume"; sessionId: SessionId }
|
|
100
108
|
| { type: "session_delete"; sessionId: SessionId };
|
|
@@ -193,6 +201,18 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
193
201
|
}
|
|
194
202
|
throw slashCommandUsageError("model");
|
|
195
203
|
}
|
|
204
|
+
if (command === "/reasoning") {
|
|
205
|
+
if (tokens.length === 1) {
|
|
206
|
+
return { type: "reasoning_status" };
|
|
207
|
+
}
|
|
208
|
+
if (tokens.length === 2 && tokens[1] === "reset") {
|
|
209
|
+
return { type: "reasoning_reset" };
|
|
210
|
+
}
|
|
211
|
+
if (tokens.length === 2) {
|
|
212
|
+
return { type: "reasoning_set", effort: tokens[1] };
|
|
213
|
+
}
|
|
214
|
+
throw slashCommandUsageError("reasoning");
|
|
215
|
+
}
|
|
196
216
|
if (command === "/resume") {
|
|
197
217
|
if (tokens.length === 1) {
|
|
198
218
|
return { type: "resume_list" };
|
|
@@ -18,6 +18,7 @@ import type { SessionId } from "../ids/runtime-id";
|
|
|
18
18
|
import { createUuidV7 } from "../ids/uuid-v7";
|
|
19
19
|
import type { ModelProfile } from "../cli/model-profiles";
|
|
20
20
|
import type { McpInventorySnapshot } from "../mcp/mcp-manager";
|
|
21
|
+
import type { ReasoningEffortSnapshot } from "../model/reasoning-effort";
|
|
21
22
|
import { SessionCatalog, type SessionSummary } from "../session/session-catalog";
|
|
22
23
|
import type { TuiProjectionStore } from "./tui-projection-store";
|
|
23
24
|
|
|
@@ -29,6 +30,9 @@ export type TuiSessionBinding = {
|
|
|
29
30
|
projectionStore: TuiProjectionStore;
|
|
30
31
|
skills(): RuntimeSkillsSnapshot;
|
|
31
32
|
mcp(): McpInventorySnapshot;
|
|
33
|
+
reasoningEffort?: () => ReasoningEffortSnapshot | undefined;
|
|
34
|
+
setReasoningEffort?: (effort: string) => ReasoningEffortSnapshot;
|
|
35
|
+
resetReasoningEffort?: () => ReasoningEffortSnapshot;
|
|
32
36
|
supportsImageInput?: () => boolean;
|
|
33
37
|
importImage?: (
|
|
34
38
|
sourcePath: string,
|
|
@@ -227,6 +231,9 @@ export function managedTuiBinding(input: {
|
|
|
227
231
|
runtimeSession: input.runtimeSession,
|
|
228
232
|
skills: () => input.runtimeSession.skills(),
|
|
229
233
|
mcp: () => input.runtimeSession.mcp(),
|
|
234
|
+
reasoningEffort: () => input.runtimeSession.reasoningEffort(),
|
|
235
|
+
setReasoningEffort: (effort) => input.runtimeSession.setReasoningEffort(effort),
|
|
236
|
+
resetReasoningEffort: () => input.runtimeSession.resetReasoningEffort(),
|
|
230
237
|
supportsImageInput: () => input.runtimeSession.supportsImageInput(),
|
|
231
238
|
importImage: (sourcePath, signal, prospectiveMessageImageCount) =>
|
|
232
239
|
input.runtimeSession.importImage(
|