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.
- package/CHANGELOG.md +79 -1
- package/README.md +81 -11
- package/package.json +5 -3
- package/src/agent/runtime-context-capabilities.ts +19 -0
- package/src/agent/runtime-context-events.ts +127 -0
- package/src/agent/runtime-context-maintenance.ts +780 -0
- package/src/agent/runtime-hosted-session.ts +443 -0
- package/src/agent/runtime-interactions.ts +291 -0
- package/src/agent/runtime-prompt-scheduler.ts +182 -0
- package/src/agent/runtime-session-contracts.ts +317 -0
- package/src/agent/runtime-session.ts +250 -2130
- package/src/agent/runtime-skills.ts +544 -0
- package/src/cli/command-line.ts +26 -2
- package/src/cli/connect-runner.tsx +26 -0
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +1 -1
- package/src/cli/public-cli-contract.ts +18 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/runner-dependencies.ts +6 -5
- package/src/cli/serve-runner.ts +45 -0
- package/src/cli/serve-runtime.ts +100 -0
- package/src/context/context-automation-policy.ts +12 -118
- package/src/context/context-swap-renderer.ts +14 -0
- package/src/events/types.ts +12 -0
- package/src/memory/memory-get-tool.ts +1 -1
- package/src/observation/observation-builder.ts +128 -48
- package/src/remote/client.ts +350 -0
- package/src/remote/config.ts +95 -0
- package/src/remote/http-server.ts +240 -0
- package/src/remote/protocol.ts +228 -0
- package/src/remote/service-store.ts +175 -0
- package/src/remote/service.ts +219 -0
- package/src/remote/sync-hub.ts +95 -0
- package/src/session/remote-history-reader.ts +143 -0
- package/src/session/resume-projection.ts +47 -21
- package/src/session/session-history-access.ts +238 -0
- package/src/session/session-store-context-readers.ts +183 -0
- package/src/session/session-store-ledger-writer.ts +315 -0
- package/src/session/session-store-record-writer.ts +318 -0
- package/src/session/session-store-recovery.ts +225 -0
- package/src/session/session-store-revisions.ts +1004 -0
- package/src/session/session-store-sql.ts +40 -0
- package/src/session/session-store-validation.ts +657 -0
- package/src/session/session-store.ts +756 -3186
- package/src/tools/bash-task.ts +46 -18
- package/src/tools/bash.ts +44 -2
- package/src/tools/glob.ts +107 -19
- package/src/tools/grep-output.ts +130 -0
- package/src/tools/grep-pagination.ts +73 -0
- package/src/tools/grep-path.ts +11 -0
- package/src/tools/grep-snippets.ts +111 -0
- package/src/tools/grep.ts +139 -154
- package/src/tools/read.ts +0 -9
- package/src/tools/recall.ts +106 -50
- package/src/tools/registry.ts +4 -6
- package/src/tools/ripgrep.ts +19 -26
- package/src/tools/shell-process.ts +30 -4
- package/src/tools/task-output-range.ts +146 -0
- package/src/tools/task-output-tool.ts +35 -5
- package/src/tools/task-output.ts +35 -0
- package/src/tools/task-stop.ts +2 -1
- package/src/tools/task-tool-args.ts +34 -0
- package/src/tools/terminal-screen.ts +11 -2
- package/src/tools/types.ts +39 -2
- package/src/tui/event-store.ts +23 -5
- 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
|
+
}
|
package/src/cli/command-line.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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;
|