tinker-agent 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/README.md +30 -1
  3. package/package.json +5 -3
  4. package/src/agent/loop.ts +50 -13
  5. package/src/agent/runtime-hosted-session.ts +443 -0
  6. package/src/agent/runtime-provider-retry.ts +115 -0
  7. package/src/agent/runtime-session-contracts.ts +11 -0
  8. package/src/agent/runtime-session.ts +29 -0
  9. package/src/cli/command-line.ts +26 -2
  10. package/src/cli/connect-runner.tsx +26 -0
  11. package/src/cli/main.ts +26 -0
  12. package/src/cli/output.ts +1 -1
  13. package/src/cli/public-cli-contract.ts +18 -0
  14. package/src/cli/public-config-contract.ts +1 -1
  15. package/src/cli/serve-runner.ts +45 -0
  16. package/src/cli/serve-runtime.ts +100 -0
  17. package/src/cli/tui-runner.tsx +1 -0
  18. package/src/context/context-swap-renderer.ts +14 -0
  19. package/src/events/types.ts +8 -0
  20. package/src/image/abortable-file-open.ts +54 -0
  21. package/src/image/image-asset-store.ts +7 -1
  22. package/src/model/fake-model-client.ts +20 -1
  23. package/src/model/openai-model-utils.ts +45 -1
  24. package/src/model/openai-responses-mapping.ts +11 -0
  25. package/src/model/openai-responses-stream.ts +14 -0
  26. package/src/observation/observation-builder.ts +87 -37
  27. package/src/remote/client.ts +350 -0
  28. package/src/remote/config.ts +95 -0
  29. package/src/remote/http-server.ts +240 -0
  30. package/src/remote/protocol.ts +228 -0
  31. package/src/remote/service-store.ts +175 -0
  32. package/src/remote/service.ts +219 -0
  33. package/src/remote/sync-hub.ts +95 -0
  34. package/src/session/remote-history-reader.ts +143 -0
  35. package/src/session/scoped-query-database.ts +27 -0
  36. package/src/session/session-history-access.ts +4 -3
  37. package/src/session/session-store.ts +7 -3
  38. package/src/tools/bash-task.ts +26 -16
  39. package/src/tools/bash.ts +44 -2
  40. package/src/tools/glob.ts +107 -19
  41. package/src/tools/grep-output.ts +130 -0
  42. package/src/tools/grep-pagination.ts +73 -0
  43. package/src/tools/grep-path.ts +11 -0
  44. package/src/tools/grep-snippets.ts +111 -0
  45. package/src/tools/grep.ts +148 -155
  46. package/src/tools/read.ts +0 -9
  47. package/src/tools/ripgrep.ts +19 -26
  48. package/src/tools/shell-process.ts +30 -4
  49. package/src/tools/task-stop.ts +2 -1
  50. package/src/tools/terminal-screen.ts +11 -2
  51. package/src/tools/types.ts +30 -2
  52. package/src/tui/app.tsx +45 -3
  53. package/src/tui/components/ask-user.tsx +15 -8
  54. package/src/tui/components/prompt-input.tsx +14 -6
  55. package/src/tui/components/timeline.tsx +17 -9
  56. package/src/tui/event-store.ts +25 -2
  57. package/src/tui/file-mention.ts +29 -5
  58. package/src/tui/remote-app.tsx +210 -0
  59. package/src/tui/tui-projection-store.ts +5 -2
  60. package/src/tui/tui-session-controller.ts +8 -0
  61. package/src/tui/workspace-file-search.ts +21 -0
@@ -0,0 +1,210 @@
1
+ import { Box, Text, useApp, useInput } from "ink";
2
+ import { TextInput } from "@inkjs/ui";
3
+ import { useEffect, useState, useSyncExternalStore } from "react";
4
+ import { RemoteClient } from "../remote/client";
5
+ import type { RemoteSessionInfo } from "../remote/protocol";
6
+
7
+ /** An explicit network client mode; the normal App/controller path is untouched. */
8
+ export function RemoteApp({ client }: { client: RemoteClient }) {
9
+ const state = useSyncExternalStore(client.subscribe, client.getSnapshot);
10
+ const { exit } = useApp();
11
+ const [workspaces, setWorkspaces] = useState<{ id: string; name: string }[]>([]);
12
+ const [sessions, setSessions] = useState<RemoteSessionInfo[]>([]);
13
+ const [workspace, setWorkspace] = useState<string | undefined>(client.workspaceId);
14
+ const [notice, setNotice] = useState(
15
+ "Select a workspace number. /workspaces returns here; /quit detaches.",
16
+ );
17
+ const [inputKey, setInputKey] = useState(0);
18
+ const [visibleCount, setVisibleCount] = useState(30);
19
+ const [browsing, setBrowsing] = useState(!client.sessionId);
20
+ const report = (error: unknown) =>
21
+ setNotice(error instanceof Error ? error.message : String(error));
22
+ useEffect(() => {
23
+ void client
24
+ .workspaces()
25
+ .then((result) => setWorkspaces(result.workspaces))
26
+ .catch(report);
27
+ }, [client]);
28
+ useInput((input, key) => {
29
+ if (key.ctrl && input === "c") exit();
30
+ });
31
+ const submit = async (text: string) => {
32
+ setInputKey((value) => value + 1);
33
+ if (!text.trim()) return;
34
+ if (text === "/quit") {
35
+ exit();
36
+ return;
37
+ }
38
+ if (text === "/workspaces") {
39
+ setWorkspace(undefined);
40
+ setBrowsing(true);
41
+ setSessions([]);
42
+ setWorkspaces((await client.workspaces()).workspaces);
43
+ return;
44
+ }
45
+ if (browsing) {
46
+ if (!workspace) {
47
+ const chosen = workspaces[Number(text) - 1];
48
+ if (!chosen) throw new Error("Enter a workspace number.");
49
+ setWorkspace(chosen.id);
50
+ setSessions((await client.sessions(chosen.id)).sessions);
51
+ setNotice(
52
+ "Enter a session number, or /new. A local session is explicitly attached when selected.",
53
+ );
54
+ } else if (text === "/new") {
55
+ await client.submit({ kind: "create", workspaceId: workspace });
56
+ setBrowsing(false);
57
+ } else {
58
+ const chosen = sessions[Number(text) - 1];
59
+ if (!chosen) throw new Error("Enter a session number or /new.");
60
+ if (chosen.owner === "local")
61
+ await client.submit({
62
+ kind: "adopt",
63
+ workspaceId: workspace,
64
+ sessionId: chosen.id,
65
+ });
66
+ else await client.select(chosen.id, workspace);
67
+ setBrowsing(false);
68
+ }
69
+ return;
70
+ }
71
+ const sessionId = client.sessionId;
72
+ if (!sessionId) throw new Error("Waiting for the service to accept the session.");
73
+ const pending = state.view?.interaction;
74
+ if (text === "/stop") {
75
+ if (!state.view?.activeRequestId) throw new Error("No active task is shown.");
76
+ await client.submit({
77
+ kind: "stop",
78
+ sessionId,
79
+ targetRequestId: state.view.activeRequestId,
80
+ });
81
+ } else if (text === "/allow" || text === "/deny") {
82
+ if (pending?.kind !== "confirmation")
83
+ throw new Error("No confirmation is pending.");
84
+ await client.submit({
85
+ kind: "confirm",
86
+ sessionId,
87
+ interactionId: pending.id,
88
+ decision: text === "/allow" ? "allow" : "deny",
89
+ });
90
+ } else if (text.startsWith("/answer ") || text === "/dismiss") {
91
+ if (pending?.kind !== "question") throw new Error("No question is pending.");
92
+ await client.submit({
93
+ kind: "answer",
94
+ sessionId,
95
+ interactionId: pending.id,
96
+ selectedIndex: text === "/dismiss" ? null : Number(text.slice(8)) - 1,
97
+ });
98
+ } else if (text === "/history") {
99
+ await client.loadOlderHistory();
100
+ setVisibleCount((count) => count + 80);
101
+ setNotice(
102
+ "Older history loaded; use your terminal scrollback to read previous renders.",
103
+ );
104
+ } else if (text.startsWith("/")) {
105
+ throw new Error(
106
+ "Commands: /stop /allow /deny /answer N /dismiss /history /workspaces /quit",
107
+ );
108
+ } else {
109
+ await client.submit({ kind: "prompt", sessionId, prompt: text });
110
+ }
111
+ };
112
+ const view = state.view;
113
+ return (
114
+ <Box flexDirection="column">
115
+ <Text bold color="cyan">
116
+ Tinker · Service connection
117
+ </Text>
118
+ <Text>
119
+ {state.connection} · {view?.status ?? "select session"} · {state.pending}{" "}
120
+ unsubmitted
121
+ </Text>
122
+ <Text dimColor>{notice}</Text>
123
+ {state.error && <Text color="red">{safe(state.error)}</Text>}
124
+ {browsing ? (
125
+ <Box flexDirection="column">
126
+ {!workspace
127
+ ? workspaces.map((w, i) => (
128
+ <Text key={w.id}>
129
+ {i + 1}. {safe(w.name)}
130
+ </Text>
131
+ ))
132
+ : sessions.map((s, i) => (
133
+ <Text key={s.id}>
134
+ {i + 1}. {safe(s.title)} · {s.status} · {s.owner}
135
+ </Text>
136
+ ))}
137
+ </Box>
138
+ ) : (
139
+ <Box flexDirection="column">
140
+ <Text bold>{safe(view?.session.title ?? "Connecting…")}</Text>
141
+ {view?.history.messages.slice(-visibleCount).map((message) => (
142
+ <Box key={message.id} flexDirection="column" marginTop={1}>
143
+ <Text bold color={message.role === "user" ? "green" : "cyan"}>
144
+ {message.name ?? message.role}
145
+ </Text>
146
+ <Text>{safe(message.text)}</Text>
147
+ {message.toolCalls?.map((call) => (
148
+ <Text key={call.id} dimColor>
149
+ {call.name} {safe(call.arguments)}
150
+ </Text>
151
+ ))}
152
+ </Box>
153
+ ))}
154
+ {view?.streaming && (
155
+ <Box flexDirection="column">
156
+ <Text dimColor>Generating…</Text>
157
+ <Text>{safe(view.streaming.text)}</Text>
158
+ </Box>
159
+ )}
160
+ {view?.tools
161
+ .filter((tool) => tool.status === "running")
162
+ .map((tool) => (
163
+ <Text key={tool.id} color="yellow">
164
+ {tool.name}: {safe(tool.arguments)}
165
+ </Text>
166
+ ))}
167
+ {view?.interaction?.kind === "question" && (
168
+ <Box flexDirection="column">
169
+ <Text color="yellow">{safe(view.interaction.question)}</Text>
170
+ {view.interaction.options.map((option, i) => (
171
+ <Text key={i}>
172
+ {i + 1}. {safe(option.description)}
173
+ </Text>
174
+ ))}
175
+ <Text>/answer N or /dismiss</Text>
176
+ </Box>
177
+ )}
178
+ {view?.interaction?.kind === "confirmation" && (
179
+ <Box flexDirection="column">
180
+ <Text color="yellow">{safe(view.interaction.command)}</Text>
181
+ <Text>{safe(view.interaction.reason)} · /allow or /deny</Text>
182
+ </Box>
183
+ )}
184
+ </Box>
185
+ )}
186
+ <Box marginTop={1}>
187
+ <Text color="cyan">› </Text>
188
+ <TextInput
189
+ key={inputKey}
190
+ placeholder={
191
+ browsing
192
+ ? "Choose a workspace/session"
193
+ : "Send task; /stop cancels; /quit detaches"
194
+ }
195
+ onSubmit={(value) => {
196
+ void submit(value).catch(report);
197
+ }}
198
+ />
199
+ </Box>
200
+ </Box>
201
+ );
202
+ }
203
+ function safe(value: string): string {
204
+ return [...value]
205
+ .filter((character) => {
206
+ const code = character.codePointAt(0)!;
207
+ return code === 10 || code === 9 || (code >= 32 && (code < 127 || code > 159));
208
+ })
209
+ .join("");
210
+ }
@@ -205,12 +205,15 @@ export class TuiProjectionStore implements EventSink, AssistantTextDeltaSink {
205
205
  return false;
206
206
  }
207
207
  this.assistantStreamAttempt = undefined;
208
- if (event.data.retryDisposition !== "scheduled" || attempt.sectionCount === 0) {
208
+ if (attempt.sectionCount === 0) {
209
209
  return false;
210
210
  }
211
211
  this.appendCommitted({
212
212
  id: `assistant-stream-retry-${attempt.iterationId}-${attempt.attemptNumber}`,
213
- text: "assistant response interrupted · retrying",
213
+ text:
214
+ event.data.retryDisposition === "scheduled"
215
+ ? "assistant response interrupted · retrying"
216
+ : "assistant response interrupted",
214
217
  status: "info",
215
218
  });
216
219
  return true;
@@ -54,6 +54,9 @@ export type TuiSessionBinding = {
54
54
  subscribeBashGuard(listener: () => void): () => void;
55
55
  setYoloMode(enabled: boolean): void;
56
56
  resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
57
+ providerRetry?: RuntimeSession["providerRetry"];
58
+ subscribeProviderRetry?: RuntimeSession["subscribeProviderRetry"];
59
+ resolveProviderRetry?: RuntimeSession["resolveProviderRetry"];
57
60
  askUser(): AskUserSnapshot;
58
61
  subscribeAskUser(listener: () => void): () => void;
59
62
  resolveAskUser(response: AskUserResolution): Promise<void>;
@@ -267,6 +270,11 @@ export function managedTuiBinding(input: {
267
270
  setYoloMode: (enabled) => input.runtimeSession.setYoloMode(enabled),
268
271
  resolveBashConfirmation: (decision) =>
269
272
  input.runtimeSession.resolveBashConfirmation(decision),
273
+ providerRetry: () => input.runtimeSession.providerRetry(),
274
+ subscribeProviderRetry: (listener) =>
275
+ input.runtimeSession.subscribeProviderRetry(listener),
276
+ resolveProviderRetry: (requestId, decision) =>
277
+ input.runtimeSession.resolveProviderRetry(requestId, decision),
270
278
  askUser: () => input.runtimeSession.askUser(),
271
279
  subscribeAskUser: (listener) => input.runtimeSession.subscribeAskUser(listener),
272
280
  resolveAskUser: (response) => input.runtimeSession.resolveAskUser(response),
@@ -93,6 +93,27 @@ export function createWorkspaceFileLister(
93
93
 
94
94
  export const listWorkspaceFiles = createWorkspaceFileLister();
95
95
 
96
+ export function deriveWorkspaceDirectories(files: readonly string[]): string[] {
97
+ const directories = new Set<string>();
98
+
99
+ for (const filePath of files) {
100
+ for (let index = 0; index < filePath.length; index += 1) {
101
+ const char = filePath[index];
102
+ if ((char === "/" || char === "\\") && index > 0) {
103
+ directories.add(filePath.slice(0, index + 1));
104
+ }
105
+ }
106
+ }
107
+
108
+ return [...directories];
109
+ }
110
+
111
+ export function listWorkspaceFilesAndDirectories(
112
+ files: readonly string[],
113
+ ): readonly string[] {
114
+ return [...files, ...deriveWorkspaceDirectories(files)];
115
+ }
116
+
96
117
  function splitPaths(stdout: string): string[] {
97
118
  return stdout
98
119
  .split("\n")