tinker-agent 2.8.0 → 2.10.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 (66) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/README.md +81 -11
  3. package/package.json +5 -3
  4. package/src/agent/runtime-context-capabilities.ts +19 -0
  5. package/src/agent/runtime-context-events.ts +127 -0
  6. package/src/agent/runtime-context-maintenance.ts +780 -0
  7. package/src/agent/runtime-hosted-session.ts +443 -0
  8. package/src/agent/runtime-interactions.ts +291 -0
  9. package/src/agent/runtime-prompt-scheduler.ts +182 -0
  10. package/src/agent/runtime-session-contracts.ts +317 -0
  11. package/src/agent/runtime-session.ts +250 -2130
  12. package/src/agent/runtime-skills.ts +544 -0
  13. package/src/cli/command-line.ts +26 -2
  14. package/src/cli/connect-runner.tsx +26 -0
  15. package/src/cli/main.ts +26 -0
  16. package/src/cli/output.ts +1 -1
  17. package/src/cli/public-cli-contract.ts +18 -0
  18. package/src/cli/public-config-contract.ts +1 -1
  19. package/src/cli/runner-dependencies.ts +6 -5
  20. package/src/cli/serve-runner.ts +45 -0
  21. package/src/cli/serve-runtime.ts +100 -0
  22. package/src/context/context-automation-policy.ts +12 -118
  23. package/src/context/context-swap-renderer.ts +14 -0
  24. package/src/events/types.ts +12 -0
  25. package/src/memory/memory-get-tool.ts +1 -1
  26. package/src/observation/observation-builder.ts +128 -48
  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/resume-projection.ts +47 -21
  36. package/src/session/session-history-access.ts +238 -0
  37. package/src/session/session-store-context-readers.ts +183 -0
  38. package/src/session/session-store-ledger-writer.ts +315 -0
  39. package/src/session/session-store-record-writer.ts +318 -0
  40. package/src/session/session-store-recovery.ts +225 -0
  41. package/src/session/session-store-revisions.ts +1004 -0
  42. package/src/session/session-store-sql.ts +40 -0
  43. package/src/session/session-store-validation.ts +657 -0
  44. package/src/session/session-store.ts +756 -3186
  45. package/src/tools/bash-task.ts +46 -18
  46. package/src/tools/bash.ts +44 -2
  47. package/src/tools/glob.ts +107 -19
  48. package/src/tools/grep-output.ts +130 -0
  49. package/src/tools/grep-pagination.ts +73 -0
  50. package/src/tools/grep-path.ts +11 -0
  51. package/src/tools/grep-snippets.ts +111 -0
  52. package/src/tools/grep.ts +139 -154
  53. package/src/tools/read.ts +0 -9
  54. package/src/tools/recall.ts +106 -50
  55. package/src/tools/registry.ts +4 -6
  56. package/src/tools/ripgrep.ts +19 -26
  57. package/src/tools/shell-process.ts +30 -4
  58. package/src/tools/task-output-range.ts +146 -0
  59. package/src/tools/task-output-tool.ts +35 -5
  60. package/src/tools/task-output.ts +35 -0
  61. package/src/tools/task-stop.ts +2 -1
  62. package/src/tools/task-tool-args.ts +34 -0
  63. package/src/tools/terminal-screen.ts +11 -2
  64. package/src/tools/types.ts +39 -2
  65. package/src/tui/event-store.ts +23 -5
  66. package/src/tui/remote-app.tsx +210 -0
@@ -0,0 +1,544 @@
1
+ import {
2
+ canonicalSequenceHash,
3
+ renderedMessageHash,
4
+ } from "../context/compiled-context-hash";
5
+ import {
6
+ commitAgentSkillsContextUpdate,
7
+ ContextManagerError,
8
+ } from "../context/context-manager";
9
+ import type { BuiltContextRequest } from "../context/context-revision";
10
+ import { ContextRevisionCompiler } from "../context/context-revision-compiler";
11
+ import {
12
+ changedContextSurfaceComponents,
13
+ contextSurfaceChangeManifestHash,
14
+ contextSurfaceChanges,
15
+ createContextSurface,
16
+ sameContextSurface,
17
+ type StoredContextSurfaceV8,
18
+ } from "../context/context-surface";
19
+ import type { ToolCompletionInput } from "../context/protocol-frame";
20
+ import { CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION } from "../context/recall-retirement-contract";
21
+ import type { AgentEventInput } from "../events/types";
22
+ import { type RuntimeIdFactory, type SessionId } from "../ids/runtime-id";
23
+ import { SessionError } from "../session/session-errors";
24
+ import type { SessionStore, StoredSkillActivation } from "../session/session-store";
25
+ import {
26
+ activeSkillManifestEntry,
27
+ skillCatalogManifest,
28
+ } from "../skills/skill-catalog";
29
+ import {
30
+ buildActiveSystemPrompt,
31
+ renderSkillActivationReceipt,
32
+ SkillActivationCoordinator,
33
+ } from "../skills/skill-context";
34
+ import type { SkillCatalogSnapshot } from "../skills/skill-loader";
35
+ import { type DefaultTooling } from "../tools/registry";
36
+ import type { ContextMeter } from "./context-meter";
37
+ import {
38
+ assertPreparedMatchesSurface,
39
+ boundedContextErrorCode,
40
+ elapsedMs,
41
+ } from "./runtime-context-events";
42
+ import {
43
+ type ContextSurfaceRefreshSummary,
44
+ type CreateRuntimeSessionInput,
45
+ type RuntimeSkillsSnapshot,
46
+ type SkillsUpdateSummary,
47
+ } from "./runtime-session-contracts";
48
+ import type { CommittedToolCompletion } from "./session-ledger";
49
+ import type { IterationIdentity } from "./types";
50
+
51
+ /** Coordinates skill activation and the corresponding persisted context surface. */
52
+ export class RuntimeSkills {
53
+ private skillCoordinator = new SkillActivationCoordinator();
54
+ get coordinator(): SkillActivationCoordinator {
55
+ return this.skillCoordinator;
56
+ }
57
+
58
+ restoreCoordinator(coordinator: SkillActivationCoordinator): void {
59
+ this.skillCoordinator = coordinator;
60
+ }
61
+
62
+ constructor(
63
+ private readonly sessionId: SessionId,
64
+ private readonly store: SessionStore,
65
+ private readonly input: Pick<
66
+ CreateRuntimeSessionInput,
67
+ "systemPrompt" | "projectInstruction" | "modelClient"
68
+ >,
69
+ private readonly skillCatalog: SkillCatalogSnapshot,
70
+ private readonly idFactory: RuntimeIdFactory,
71
+ private readonly contextMeter: ContextMeter,
72
+ private readonly toolDefinitions: DefaultTooling["registry"]["definitions"],
73
+ private readonly append: (event: AgentEventInput) => Promise<void>,
74
+ ) {}
75
+
76
+ async refreshContextSurface(
77
+ candidateSurface: StoredContextSurfaceV8,
78
+ ): Promise<ContextSurfaceRefreshSummary | undefined> {
79
+ const snapshot = this.store.loadContextSnapshot();
80
+ if (sameContextSurface(snapshot.surface, candidateSurface)) {
81
+ return undefined;
82
+ }
83
+
84
+ const changes = contextSurfaceChanges(snapshot.surface, candidateSurface);
85
+ const changed = changedContextSurfaceComponents(changes);
86
+ if (changed.length === 0) {
87
+ throw new Error("Changed context surface has an empty change manifest.");
88
+ }
89
+ const startedAt = performance.now();
90
+ await this.append({
91
+ type: "context.revision.started",
92
+ sessionId: this.sessionId,
93
+ data: {
94
+ strategy: "surface_refresh",
95
+ reason: "resume",
96
+ baseRevisionNumber: snapshot.revision.revisionNumber,
97
+ changed,
98
+ },
99
+ });
100
+
101
+ let stage: "prepare" | "commit" | "activate" = "prepare";
102
+ let committed = false;
103
+ try {
104
+ const compiler = new ContextRevisionCompiler();
105
+ const active = compiler.compileActive(snapshot);
106
+ const candidateCompiled = compiler.compileProspective({
107
+ active,
108
+ canonical: snapshot.canonical,
109
+ activeOverrides: snapshot.activeOverrides,
110
+ addedOverrides: [],
111
+ activeSurface: snapshot.surface,
112
+ surface: candidateSurface,
113
+ });
114
+ const prepared = this.input.modelClient.prepare({
115
+ messages: candidateCompiled.entries.map((entry) => entry.message),
116
+ tools: [...candidateSurface.toolDefinitions],
117
+ });
118
+ assertPreparedMatchesSurface(prepared, candidateSurface);
119
+
120
+ stage = "commit";
121
+ const revision = this.store.commitSurfaceRefresh({
122
+ revisionId: this.idFactory.createContextRevisionId(),
123
+ expectedBaseRevisionId: snapshot.revision.revisionId,
124
+ expectedBaseRevisionNumber: snapshot.revision.revisionNumber,
125
+ expectedCanonicalThroughOrdinal: snapshot.canonical.messages.length,
126
+ expectedBaseActiveOverrideManifestSha256:
127
+ snapshot.revision.activeOverrideManifestSha256,
128
+ surface: candidateSurface,
129
+ changes,
130
+ changeManifestSha256: contextSurfaceChangeManifestHash(changes),
131
+ canonicalSequenceSha256: canonicalSequenceHash(snapshot.canonical),
132
+ renderedMessageSha256: renderedMessageHash(candidateCompiled.entries),
133
+ });
134
+ committed = true;
135
+
136
+ stage = "activate";
137
+ this.contextMeter.startRevision({
138
+ reason: "context_rebuilt",
139
+ requestConfigHash: prepared.requestConfigHash,
140
+ toolSchemaHash: prepared.toolSchemaHash,
141
+ });
142
+ const summary = Object.freeze({
143
+ previousRevisionNumber: snapshot.revision.revisionNumber,
144
+ revisionNumber: revision.revisionNumber,
145
+ changed,
146
+ toolCountBefore: snapshot.surface.toolDefinitions.length,
147
+ toolCountAfter: candidateSurface.toolDefinitions.length,
148
+ });
149
+ await this.append({
150
+ type: "context.revision.finished",
151
+ sessionId: this.sessionId,
152
+ data: {
153
+ strategy: "surface_refresh",
154
+ reason: "resume",
155
+ baseRevisionNumber: summary.previousRevisionNumber,
156
+ revisionNumber: summary.revisionNumber,
157
+ changed: summary.changed,
158
+ toolCountBefore: summary.toolCountBefore,
159
+ toolCountAfter: summary.toolCountAfter,
160
+ measuredAnchorCleared: true,
161
+ durationMs: elapsedMs(startedAt),
162
+ },
163
+ });
164
+ return summary;
165
+ } catch (error) {
166
+ await this.append({
167
+ type: "context.revision.failed",
168
+ sessionId: this.sessionId,
169
+ data: {
170
+ strategy: "surface_refresh",
171
+ reason: "resume",
172
+ stage,
173
+ errorCode: boundedContextErrorCode(
174
+ error instanceof SessionError
175
+ ? error.code
176
+ : error instanceof Error
177
+ ? error.name
178
+ : "CONTEXT_SURFACE_REFRESH_FAILED",
179
+ ),
180
+ error: `Context surface refresh failed at ${stage}.`,
181
+ committed,
182
+ },
183
+ }).catch(() => undefined);
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ skills(): RuntimeSkillsSnapshot {
189
+ const activeNames = new Set(
190
+ this.skillCoordinator.activeEntries().map((entry) => entry.skill.name),
191
+ );
192
+ return Object.freeze({
193
+ skills: Object.freeze(
194
+ [...this.skillCatalog.skills.values()]
195
+ .sort((left, right) => compareText(left.name, right.name))
196
+ .map((skill) =>
197
+ Object.freeze({
198
+ name: skill.name,
199
+ description: skill.description,
200
+ scope: skill.scope,
201
+ active: activeNames.has(skill.name),
202
+ }),
203
+ ),
204
+ ),
205
+ shadowedNames: Object.freeze(
206
+ this.skillCatalog.shadowed.map((entry) => entry.name),
207
+ ),
208
+ });
209
+ }
210
+
211
+ appendSkillsCatalogLoaded(): Promise<void> {
212
+ const activeNames = this.skillCoordinator
213
+ .activeEntries()
214
+ .map((entry) => entry.skill.name);
215
+ if (
216
+ this.skillCatalog.skills.size === 0 &&
217
+ activeNames.length === 0 &&
218
+ this.skillCatalog.shadowed.length === 0
219
+ ) {
220
+ return Promise.resolve();
221
+ }
222
+ const skills = [...this.skillCatalog.skills.values()];
223
+ return this.append({
224
+ type: "skills.catalog.loaded",
225
+ sessionId: this.sessionId,
226
+ data: {
227
+ availableCount: skills.length,
228
+ projectCount: skills.filter((skill) => skill.scope === "project").length,
229
+ userCount: skills.filter((skill) => skill.scope === "user").length,
230
+ activeNames: Object.freeze(activeNames),
231
+ shadowedNames: Object.freeze(
232
+ this.skillCatalog.shadowed.map((entry) => entry.name),
233
+ ),
234
+ },
235
+ });
236
+ }
237
+
238
+ onToolCompletionsCommitted(input: {
239
+ completions: readonly ToolCompletionInput[];
240
+ committed: readonly CommittedToolCompletion[];
241
+ }): void {
242
+ if (input.completions.length !== input.committed.length) {
243
+ throw new Error("Committed tool completion identity count does not match.");
244
+ }
245
+ for (let index = 0; index < input.completions.length; index += 1) {
246
+ const completion = input.completions[index];
247
+ const committed = input.committed[index];
248
+ if (
249
+ completion === undefined ||
250
+ committed === undefined ||
251
+ completion.call.toolCallId !== committed.toolCallId
252
+ ) {
253
+ throw new Error("Committed tool completion identity is invalid.");
254
+ }
255
+ if (
256
+ completion.kind === "returned" &&
257
+ completion.raw.kind === "skill" &&
258
+ completion.raw.ok &&
259
+ completion.raw.status === "loaded"
260
+ ) {
261
+ this.skillCoordinator.markPending(completion.raw.name);
262
+ }
263
+ }
264
+ }
265
+
266
+ async commitSkillSettlements(input: {
267
+ reason: "activation" | "resume";
268
+ unresolved: readonly StoredSkillActivation[];
269
+ candidateSurface?: StoredContextSurfaceV8;
270
+ activated?: readonly string[];
271
+ refreshed?: readonly string[];
272
+ deactivated?: readonly string[];
273
+ }): Promise<SkillsUpdateSummary> {
274
+ if (input.unresolved.length === 0) {
275
+ throw new Error("Agent Skills update requires unresolved activations.");
276
+ }
277
+ const snapshot = this.store.loadContextSnapshot();
278
+ const canonicalMessages = new Map(
279
+ snapshot.canonical.messages.map((message) => [message.messageId, message]),
280
+ );
281
+ const activeByName = new Map(
282
+ this.skillCoordinator
283
+ .activeEntries()
284
+ .map((entry) => [entry.skill.name, entry] as const),
285
+ );
286
+ const activated = new Set(input.activated ?? []);
287
+ const unavailable = new Set<string>();
288
+ const settlements: Array<{
289
+ activationMessageId: StoredSkillActivation["activationMessageId"];
290
+ name: string;
291
+ state: "promoted" | "rejected";
292
+ rejectionReason?: string;
293
+ }> = [];
294
+ const receipts = [];
295
+ for (const activation of [...input.unresolved].sort((left, right) =>
296
+ compareText(left.name, right.name),
297
+ )) {
298
+ const skill = this.skillCatalog.skills.get(activation.name);
299
+ const canPromote = activation.state === "dispatched" && skill !== undefined;
300
+ if (canPromote) {
301
+ const existing = activeByName.get(activation.name);
302
+ if (
303
+ existing !== undefined &&
304
+ existing.activationMessageId !== activation.activationMessageId
305
+ ) {
306
+ throw new Error(
307
+ `Agent Skill ${activation.name} already has another active activation.`,
308
+ );
309
+ }
310
+ activeByName.set(activation.name, {
311
+ skill,
312
+ activationMessageId: activation.activationMessageId,
313
+ });
314
+ activated.add(activation.name);
315
+ }
316
+ const state = canPromote ? "promoted" : "rejected";
317
+ const rejectionReason =
318
+ state === "promoted"
319
+ ? undefined
320
+ : activation.state === "pending"
321
+ ? "not_dispatched"
322
+ : "unavailable";
323
+ if (rejectionReason === "unavailable") {
324
+ unavailable.add(activation.name);
325
+ }
326
+ settlements.push({
327
+ activationMessageId: activation.activationMessageId,
328
+ name: activation.name,
329
+ state,
330
+ ...(rejectionReason === undefined ? {} : { rejectionReason }),
331
+ });
332
+ const message = canonicalMessages.get(activation.activationMessageId);
333
+ if (message?.role !== "tool") {
334
+ throw new Error(
335
+ `Agent Skill activation message ${activation.activationMessageId} is missing.`,
336
+ );
337
+ }
338
+ receipts.push(
339
+ renderSkillActivationReceipt({
340
+ message: {
341
+ messageId: message.messageId,
342
+ frameId: message.frameId,
343
+ ordinal: message.ordinal,
344
+ content: message.displayText,
345
+ contentSha256: message.contentSha256,
346
+ },
347
+ name: activation.name,
348
+ outcome:
349
+ state === "promoted"
350
+ ? "promoted"
351
+ : rejectionReason === "unavailable"
352
+ ? "unavailable"
353
+ : "rejected",
354
+ }),
355
+ );
356
+ }
357
+ const nextActive = Object.freeze(
358
+ [...activeByName.values()].sort((left, right) =>
359
+ compareText(left.skill.name, right.skill.name),
360
+ ),
361
+ );
362
+ const createdAt = new Date().toISOString();
363
+ const definitions = this.toolDefinitions();
364
+ const renderedSystemPrompt = buildActiveSystemPrompt({
365
+ baseSystemPrompt: this.input.systemPrompt,
366
+ activeSkills: nextActive,
367
+ });
368
+ const surfacePrepared = this.input.modelClient.prepare({
369
+ messages: [{ role: "system", content: renderedSystemPrompt }],
370
+ tools: definitions,
371
+ });
372
+ const generatedSurface =
373
+ input.candidateSurface ??
374
+ createContextSurface({
375
+ surfaceId: this.idFactory.createContextSurfaceId(),
376
+ sessionId: this.sessionId,
377
+ systemPrompt: renderedSystemPrompt,
378
+ recallContractVersion: CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION,
379
+ ...(this.input.projectInstruction === undefined
380
+ ? {}
381
+ : { projectInstruction: this.input.projectInstruction }),
382
+ skillCatalog: skillCatalogManifest(this.skillCatalog.skills.values()),
383
+ activeSkills: nextActive.map((entry) =>
384
+ activeSkillManifestEntry(entry.skill, entry.activationMessageId),
385
+ ),
386
+ toolDefinitions: definitions,
387
+ prepared: surfacePrepared,
388
+ createdAt,
389
+ });
390
+ assertPreparedMatchesSurface(surfacePrepared, generatedSurface);
391
+ const surface = sameContextSurface(snapshot.surface, generatedSurface)
392
+ ? snapshot.surface
393
+ : generatedSurface;
394
+ const startedAt = performance.now();
395
+ await this.append({
396
+ type: "context.revision.started",
397
+ sessionId: this.sessionId,
398
+ data: {
399
+ strategy: "skills_update",
400
+ reason: input.reason,
401
+ baseRevisionNumber: snapshot.revision.revisionNumber,
402
+ names: Object.freeze(
403
+ input.unresolved.map((entry) => entry.name).sort(compareText),
404
+ ),
405
+ },
406
+ });
407
+ let stage: "prepare" | "commit" | "activate" = "prepare";
408
+ let committed = false;
409
+ try {
410
+ const revision = commitAgentSkillsContextUpdate({
411
+ store: this.store,
412
+ contextMeter: this.contextMeter,
413
+ idFactory: this.idFactory,
414
+ snapshot,
415
+ surface,
416
+ addedOverrides: receipts,
417
+ settlements,
418
+ });
419
+ committed = true;
420
+ stage = "activate";
421
+ this.skillCoordinator.replaceActive(nextActive);
422
+ this.skillCoordinator.settle(
423
+ input.unresolved.map((activation) => activation.name),
424
+ );
425
+ const summary = Object.freeze({
426
+ previousRevisionNumber: snapshot.revision.revisionNumber,
427
+ revisionNumber: revision.revisionNumber,
428
+ activated: Object.freeze([...activated].sort()),
429
+ refreshed: Object.freeze([...(input.refreshed ?? [])].sort()),
430
+ deactivated: Object.freeze([...(input.deactivated ?? [])].sort()),
431
+ unavailable: Object.freeze([...unavailable].sort()),
432
+ addedOverrideCount: receipts.length,
433
+ });
434
+ await this.append({
435
+ type: "context.revision.finished",
436
+ sessionId: this.sessionId,
437
+ data: {
438
+ strategy: "skills_update",
439
+ reason: input.reason,
440
+ baseRevisionNumber: summary.previousRevisionNumber,
441
+ revisionNumber: summary.revisionNumber,
442
+ activated: summary.activated,
443
+ refreshed: summary.refreshed,
444
+ deactivated: summary.deactivated,
445
+ unavailable: summary.unavailable,
446
+ addedOverrideCount: summary.addedOverrideCount,
447
+ measuredAnchorCleared: true,
448
+ durationMs: elapsedMs(startedAt),
449
+ },
450
+ });
451
+ return summary;
452
+ } catch (error) {
453
+ if (error instanceof ContextManagerError) {
454
+ committed = error.committed;
455
+ stage =
456
+ error.stage === "commit"
457
+ ? "commit"
458
+ : error.stage === "activate"
459
+ ? "activate"
460
+ : "prepare";
461
+ }
462
+ await this.append({
463
+ type: "context.revision.failed",
464
+ sessionId: this.sessionId,
465
+ data: {
466
+ strategy: "skills_update",
467
+ reason: input.reason,
468
+ stage,
469
+ errorCode: boundedContextErrorCode(
470
+ error instanceof ContextManagerError
471
+ ? error.code
472
+ : error instanceof SessionError
473
+ ? error.code
474
+ : error instanceof Error
475
+ ? error.name
476
+ : "SKILLS_UPDATE_VALIDATION_FAILED",
477
+ ),
478
+ error: `Agent Skills update failed at ${stage}.`,
479
+ committed,
480
+ },
481
+ }).catch(() => undefined);
482
+ throw error;
483
+ }
484
+ }
485
+
486
+ async settleClosedTurnSkills(): Promise<void> {
487
+ const unresolved = this.store.loadSkillActivations(["pending", "dispatched"]);
488
+ if (unresolved.length === 0) {
489
+ return;
490
+ }
491
+ const summary = await this.commitSkillSettlements({
492
+ reason: "activation",
493
+ unresolved,
494
+ });
495
+ await this.append({
496
+ type: "skills.updated",
497
+ sessionId: this.sessionId,
498
+ data: {
499
+ reason: "activation",
500
+ activated: summary.activated,
501
+ refreshed: summary.refreshed,
502
+ deactivated: summary.deactivated,
503
+ unavailable: summary.unavailable,
504
+ revisionNumber: summary.revisionNumber,
505
+ },
506
+ });
507
+ }
508
+
509
+ markModelDispatch(input: {
510
+ iteration: IterationIdentity;
511
+ built: BuiltContextRequest;
512
+ }): void {
513
+ const pending = this.store.loadSkillActivations(["pending"]);
514
+ if (pending.length === 0) {
515
+ return;
516
+ }
517
+ const visibleCanonicalMessageIds = new Set(
518
+ input.built.compiled.entries
519
+ .filter(
520
+ (entry) =>
521
+ entry.representation === "canonical" && entry.message.role === "tool",
522
+ )
523
+ .map((entry) => entry.messageId),
524
+ );
525
+ const included = pending.filter((activation) =>
526
+ visibleCanonicalMessageIds.has(activation.activationMessageId),
527
+ );
528
+ if (included.length === 0) {
529
+ return;
530
+ }
531
+ const dispatched = this.store.markSkillActivationsDispatched({
532
+ iterationId: input.iteration.iterationId,
533
+ activationMessageIds: included.map(
534
+ (activation) => activation.activationMessageId,
535
+ ),
536
+ });
537
+ this.skillCoordinator.markDispatched(
538
+ dispatched.map((activation) => activation.name),
539
+ );
540
+ }
541
+ }
542
+ function compareText(left: string, right: string): number {
543
+ return left < right ? -1 : left > right ? 1 : 0;
544
+ }
@@ -6,6 +6,8 @@ import { CliUsageError, type CliCommandScope } from "./output";
6
6
  export type CliCommand =
7
7
  | { readonly type: "tui"; readonly profileName?: string }
8
8
  | { readonly type: "update" }
9
+ | { readonly type: "serve"; readonly configPath: string }
10
+ | { readonly type: "connect"; readonly configPath: string }
9
11
  | {
10
12
  readonly type: "run";
11
13
  readonly profileName?: string;
@@ -130,6 +132,21 @@ export async function parseCommandLine(
130
132
  },
131
133
  );
132
134
 
135
+ for (const type of ["serve", "connect"] as const) {
136
+ const command = contract[type];
137
+ program
138
+ .command(command.command)
139
+ .description(command.description)
140
+ .requiredOption(command.configOption.flags, command.configOption.description)
141
+ .allowExcessArguments(false)
142
+ .exitOverride()
143
+ .action((options: { config: string }) => {
144
+ if (!options.config.trim())
145
+ throw new CliUsageError("--config requires a non-empty path.", type);
146
+ selectedCommand = Object.freeze({ type, configPath: options.config });
147
+ });
148
+ }
149
+
133
150
  program
134
151
  .command(contract.update.command)
135
152
  .description(contract.update.description)
@@ -166,10 +183,13 @@ export async function parseCommandLine(
166
183
  "run",
167
184
  );
168
185
  }
169
- if (selectedCommand.type === "update" && topLevelProfile !== undefined) {
186
+ if (
187
+ ["update", "serve", "connect"].includes(selectedCommand.type) &&
188
+ topLevelProfile !== undefined
189
+ ) {
170
190
  throw new CliUsageError(
171
191
  "The top-level --profile option only applies to the TUI.",
172
- "update",
192
+ selectedCommand.type as CliCommandScope,
173
193
  );
174
194
  }
175
195
  return Object.freeze({ type: "command", command: selectedCommand });
@@ -224,6 +244,10 @@ function preflightArgv(args: readonly string[]): CliCommandScope {
224
244
  scope = "update";
225
245
  continue;
226
246
  }
247
+ if (token === "serve" || token === "connect") {
248
+ scope = token;
249
+ continue;
250
+ }
227
251
  if (token === "help") {
228
252
  return "root";
229
253
  }
@@ -0,0 +1,26 @@
1
+ import { render } from "ink";
2
+ import { RemoteClient, loadRemoteClientConfig } from "../remote/client";
3
+ import { RemoteApp } from "../tui/remote-app";
4
+ import type { CliOutputWriter } from "./output";
5
+
6
+ export async function runConnect(input: {
7
+ configPath: string;
8
+ env: NodeJS.ProcessEnv;
9
+ stdout: CliOutputWriter;
10
+ }): Promise<number> {
11
+ if (!process.stdin.isTTY)
12
+ throw new Error("tinker connect requires an interactive terminal.");
13
+ const client = new RemoteClient(await loadRemoteClientConfig(input.configPath));
14
+ let instance: ReturnType<typeof render> | undefined;
15
+ try {
16
+ await client.initialize();
17
+ instance = render(<RemoteApp client={client} />, { incrementalRendering: true });
18
+ await instance.waitUntilExit();
19
+ return 0;
20
+ } finally {
21
+ instance?.unmount();
22
+ await client.close();
23
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
24
+ process.stdin.pause();
25
+ }
26
+ }
package/src/cli/main.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SessionId } from "../ids/runtime-id";
2
+ import path from "node:path";
2
3
  import { createUuidV7 } from "../ids/uuid-v7";
3
4
  import { parseCommandLine, type CommandLineResult } from "./command-line";
4
5
  import type {
@@ -91,6 +92,8 @@ export type MainDependencies = {
91
92
  readonly loadTuiRunner: () => Promise<TuiRunner>;
92
93
  readonly loadOneShotRunner: () => Promise<OneShotRunner>;
93
94
  readonly loadUpdateRunner: () => Promise<UpdateRunner>;
95
+ readonly loadServeRunner: () => Promise<typeof import("./serve-runner")>;
96
+ readonly loadConnectRunner: () => Promise<typeof import("./connect-runner")>;
94
97
  };
95
98
 
96
99
  const DEFAULT_DEPENDENCIES: MainDependencies = {
@@ -102,6 +105,8 @@ const DEFAULT_DEPENDENCIES: MainDependencies = {
102
105
  loadTuiRunner: () => import("./tui-runner"),
103
106
  loadOneShotRunner: () => import("./run-runner"),
104
107
  loadUpdateRunner: () => import("./update-runner"),
108
+ loadServeRunner: () => import("./serve-runner"),
109
+ loadConnectRunner: () => import("./connect-runner"),
105
110
  };
106
111
 
107
112
  export async function main(
@@ -160,6 +165,27 @@ export async function main(
160
165
  }
161
166
  }
162
167
 
168
+ if (parsed.command.type === "serve" || parsed.command.type === "connect") {
169
+ const options = {
170
+ configPath: path.resolve(cwd, parsed.command.configPath),
171
+ env,
172
+ stdout: input.stdout,
173
+ };
174
+ try {
175
+ const exitCode =
176
+ parsed.command.type === "serve"
177
+ ? await (await dependencies.loadServeRunner()).runServe(options)
178
+ : await (await dependencies.loadConnectRunner()).runConnect(options);
179
+ return finish(exitCode);
180
+ } catch (error) {
181
+ await writeCliOutput(
182
+ input.stderr,
183
+ renderCliFailure("Remote operation failed", error),
184
+ );
185
+ return finish(1);
186
+ }
187
+ }
188
+
163
189
  let configBoundary: ConfigBoundary;
164
190
  let publicConfig: ResolvedPublicConfig;
165
191
  let runnerConfig: RunnerConfig;
package/src/cli/output.ts CHANGED
@@ -4,7 +4,7 @@ const TRUNCATION_MARKER = "...[truncated]";
4
4
  const ESCAPE = String.fromCharCode(27);
5
5
  const ANSI_CSI_PATTERN = new RegExp(`${ESCAPE}\\[[0-?]*[ -/]*[@-~]`, "g");
6
6
 
7
- export type CliCommandScope = "root" | "run" | "update";
7
+ export type CliCommandScope = "root" | "run" | "update" | "serve" | "connect";
8
8
 
9
9
  export interface CliOutputWriter {
10
10
  write(chunk: string): boolean | void;