tinker-agent 1.9.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +64 -6
  3. package/package.json +1 -1
  4. package/src/agent/loop.ts +13 -0
  5. package/src/agent/runtime-session.ts +165 -0
  6. package/src/agent/session-ledger.ts +20 -3
  7. package/src/cli/config.ts +11 -2
  8. package/src/cli/model-profiles.ts +58 -0
  9. package/src/cli/public-config-contract.ts +73 -7
  10. package/src/cli/run-runner.ts +4 -1
  11. package/src/cli/runner-dependencies.ts +28 -4
  12. package/src/cli/tui-memory.ts +4 -0
  13. package/src/cli/tui-runner.tsx +8 -1
  14. package/src/context/context-automation-policy.ts +22 -21
  15. package/src/context/context-manager.ts +91 -15
  16. package/src/context/context-policy.ts +0 -2
  17. package/src/context/context-swap-renderer.ts +1 -1
  18. package/src/context/prefix-retirement-planner.ts +58 -8
  19. package/src/context/recall-retirement-contract.ts +5 -4
  20. package/src/context/swap-planner.ts +33 -27
  21. package/src/model/fake-model-client.ts +26 -16
  22. package/src/model/model-api.ts +12 -0
  23. package/src/model/model-client.ts +9 -1
  24. package/src/model/moonshot-input-token-estimator.ts +5 -1
  25. package/src/model/openai-chat-mapping.ts +2 -24
  26. package/src/model/openai-chat-model-client.ts +18 -294
  27. package/src/model/openai-image-mapping.ts +20 -0
  28. package/src/model/openai-model-utils.ts +304 -0
  29. package/src/model/openai-responses-mapping.ts +532 -0
  30. package/src/model/openai-responses-model-client.ts +295 -0
  31. package/src/model/openai-responses-stream.ts +96 -0
  32. package/src/model/openai-responses-token-estimator.ts +155 -0
  33. package/src/model/reasoning-effort.ts +60 -0
  34. package/src/session/session-catalog.ts +2 -2
  35. package/src/session/session-history-reader.ts +6 -1
  36. package/src/session/session-schema.ts +268 -4
  37. package/src/session/session-store.ts +105 -26
  38. package/src/skills/skill-context.ts +2 -2
  39. package/src/tools/bounded-output-preview.ts +276 -0
  40. package/src/tools/recall.ts +67 -36
  41. package/src/tools/registry.ts +7 -2
  42. package/src/tools/task-output-snapshot.ts +6 -22
  43. package/src/tools/task-output.ts +23 -27
  44. package/src/tui/app.tsx +82 -5
  45. package/src/tui/components/prompt-input.tsx +9 -1
  46. package/src/tui/slash-commands.ts +20 -0
  47. package/src/tui/tui-session-controller.ts +7 -0
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 = canSwitchModel
187
- ? SLASH_COMMANDS
188
- : SLASH_COMMANDS.filter((command) => command.name !== "model");
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 Recall.`;
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 Recall.`;
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 {
@@ -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} · {formatWorkspacePath(props.workspaceRoot)}
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>
@@ -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(