tinker-agent 1.0.65 → 1.2.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 +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -4,11 +4,12 @@ import type { ProtocolContextView } from "../context/protocol-frame";
|
|
|
4
4
|
import type { BuiltContextRequest } from "../context/context-revision";
|
|
5
5
|
import {
|
|
6
6
|
InMemorySessionLedger,
|
|
7
|
+
type AdmissionBaseToken,
|
|
7
8
|
type AgentTurnLedger,
|
|
8
9
|
type PendingLedgerTurn,
|
|
9
10
|
type SessionLedger,
|
|
10
11
|
} from "../agent/session-ledger";
|
|
11
|
-
import type { RunAgentResult, TurnIdentity } from "../agent/types";
|
|
12
|
+
import type { RunAgentResult, TurnIdentity, UserMessage } from "../agent/types";
|
|
12
13
|
import type { SessionStore } from "./session-store";
|
|
13
14
|
|
|
14
15
|
export class SqliteSessionLedger implements SessionLedger {
|
|
@@ -20,7 +21,11 @@ export class SqliteSessionLedger implements SessionLedger {
|
|
|
20
21
|
private readonly idFactory: RuntimeIdFactory,
|
|
21
22
|
) {}
|
|
22
23
|
|
|
23
|
-
beginTurn(input: {
|
|
24
|
+
beginTurn(input: {
|
|
25
|
+
turn: TurnIdentity;
|
|
26
|
+
userMessage: UserMessage;
|
|
27
|
+
admissionBase?: AdmissionBaseToken;
|
|
28
|
+
}): PendingLedgerTurn {
|
|
24
29
|
this.requireAvailable("begin a turn");
|
|
25
30
|
if (this.active !== undefined) {
|
|
26
31
|
throw new Error("Cannot begin a SQLite ledger turn while another turn is open.");
|
|
@@ -47,14 +52,14 @@ export class SqliteSessionLedger implements SessionLedger {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
buildCandidateModelRequest(
|
|
50
|
-
|
|
55
|
+
userMessage: UserMessage,
|
|
51
56
|
tools: readonly ToolDefinition[],
|
|
52
57
|
): BuiltContextRequest {
|
|
53
58
|
this.requireAvailable("build candidate context");
|
|
54
59
|
if (this.active !== undefined) {
|
|
55
60
|
throw new Error("Cannot build candidate context while a turn is open.");
|
|
56
61
|
}
|
|
57
|
-
return this.createCore().buildCandidateModelRequest(
|
|
62
|
+
return this.createCore().buildCandidateModelRequest(userMessage, tools);
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
committedMessageCount(): number {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { MessageId } from "../ids/runtime-id";
|
|
2
|
+
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
3
|
+
import type { ToolDefinition } from "../tools/types";
|
|
4
|
+
import { immutableCanonicalClone, immutableRecord } from "../context/protocol-frame";
|
|
5
|
+
import {
|
|
6
|
+
SKILL_CATALOG_MAX_BYTES,
|
|
7
|
+
SkillError,
|
|
8
|
+
type LoadedSkill,
|
|
9
|
+
type SkillCatalogSnapshot,
|
|
10
|
+
type SkillScope,
|
|
11
|
+
type SkillShadow,
|
|
12
|
+
} from "./skill-loader";
|
|
13
|
+
|
|
14
|
+
export type SkillCatalogManifestEntry = {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly scope: SkillScope;
|
|
17
|
+
readonly directorySha256: string;
|
|
18
|
+
readonly descriptionSha256: string;
|
|
19
|
+
readonly skillFileSha256: string;
|
|
20
|
+
readonly byteLength: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ActiveSkillManifestEntry = SkillCatalogManifestEntry & {
|
|
24
|
+
readonly activationMessageId: MessageId;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function createSkillCatalogSnapshot(input: {
|
|
28
|
+
workspaceRoot: string;
|
|
29
|
+
homeRoot: string;
|
|
30
|
+
skills: readonly LoadedSkill[];
|
|
31
|
+
shadowed: readonly SkillShadow[];
|
|
32
|
+
}): SkillCatalogSnapshot {
|
|
33
|
+
const sorted = [...input.skills].sort((left, right) =>
|
|
34
|
+
compareText(left.name, right.name),
|
|
35
|
+
);
|
|
36
|
+
if (new Set(sorted.map((skill) => skill.name)).size !== sorted.length) {
|
|
37
|
+
throw new SkillError(
|
|
38
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
39
|
+
"Agent Skill catalog contains duplicate winning names.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const skills = new ImmutableMap(sorted.map((skill) => [skill.name, skill] as const));
|
|
43
|
+
const manifest = skillCatalogManifest(sorted);
|
|
44
|
+
const snapshot: SkillCatalogSnapshot = Object.freeze({
|
|
45
|
+
workspaceRoot: input.workspaceRoot,
|
|
46
|
+
homeRoot: input.homeRoot,
|
|
47
|
+
skills,
|
|
48
|
+
shadowed: Object.freeze([...input.shadowed]),
|
|
49
|
+
manifestSha256: sha256(stableJsonStringify(manifest)),
|
|
50
|
+
});
|
|
51
|
+
if (skills.size > 0) {
|
|
52
|
+
const definition = createSkillToolDefinition(snapshot);
|
|
53
|
+
const bytes = Buffer.byteLength(stableJsonStringify(definition), "utf8");
|
|
54
|
+
if (bytes > SKILL_CATALOG_MAX_BYTES) {
|
|
55
|
+
throw new SkillError(
|
|
56
|
+
"SKILL_CATALOG_TOO_LARGE",
|
|
57
|
+
`Agent Skill catalog is ${bytes} bytes; the limit is ${SKILL_CATALOG_MAX_BYTES} bytes.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return snapshot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function skillCatalogManifest(
|
|
65
|
+
skills: Iterable<LoadedSkill>,
|
|
66
|
+
): readonly SkillCatalogManifestEntry[] {
|
|
67
|
+
return Object.freeze(
|
|
68
|
+
[...skills]
|
|
69
|
+
.sort((left, right) => compareText(left.name, right.name))
|
|
70
|
+
.map((skill) => skillManifestEntry(skill)),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function skillManifestEntry(skill: LoadedSkill): SkillCatalogManifestEntry {
|
|
75
|
+
return immutableRecord({
|
|
76
|
+
name: skill.name,
|
|
77
|
+
scope: skill.scope,
|
|
78
|
+
directorySha256: sha256(skill.directory),
|
|
79
|
+
descriptionSha256: sha256(skill.description),
|
|
80
|
+
skillFileSha256: skill.sha256,
|
|
81
|
+
byteLength: skill.byteLength,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function activeSkillManifestEntry(
|
|
86
|
+
skill: LoadedSkill,
|
|
87
|
+
activationMessageId: MessageId,
|
|
88
|
+
): ActiveSkillManifestEntry {
|
|
89
|
+
return immutableRecord({
|
|
90
|
+
...skillManifestEntry(skill),
|
|
91
|
+
activationMessageId,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function skillManifestSha256(
|
|
96
|
+
manifest: readonly SkillCatalogManifestEntry[],
|
|
97
|
+
): string {
|
|
98
|
+
return sha256(stableJsonStringify(manifest));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function activeSkillManifestSha256(
|
|
102
|
+
manifest: readonly ActiveSkillManifestEntry[],
|
|
103
|
+
): string {
|
|
104
|
+
return sha256(stableJsonStringify(manifest));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createSkillToolDefinition(
|
|
108
|
+
catalog: SkillCatalogSnapshot,
|
|
109
|
+
): ToolDefinition {
|
|
110
|
+
const skills = [...catalog.skills.values()].sort((left, right) =>
|
|
111
|
+
compareText(left.name, right.name),
|
|
112
|
+
);
|
|
113
|
+
if (skills.length === 0) {
|
|
114
|
+
throw new SkillError(
|
|
115
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
116
|
+
"Cannot create the Skill tool for an empty catalog.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const lines = skills.map((skill) =>
|
|
120
|
+
stableJsonStringify({
|
|
121
|
+
description: skill.description,
|
|
122
|
+
name: skill.name,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
return immutableCanonicalClone({
|
|
126
|
+
name: "Skill",
|
|
127
|
+
description: [
|
|
128
|
+
"Load specialized Agent Skills instructions before proceeding when a task matches.",
|
|
129
|
+
"Available skills (one JSON object per line):",
|
|
130
|
+
...lines,
|
|
131
|
+
].join("\n"),
|
|
132
|
+
parameters: {
|
|
133
|
+
type: "object",
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
properties: {
|
|
136
|
+
name: {
|
|
137
|
+
type: "string",
|
|
138
|
+
enum: skills.map((skill) => skill.name),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ["name"],
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function compareText(left: string, right: string): number {
|
|
147
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
class ImmutableMap<K, V> extends Map<K, V> {
|
|
151
|
+
private locked = false;
|
|
152
|
+
|
|
153
|
+
constructor(entries: readonly (readonly [K, V])[]) {
|
|
154
|
+
super();
|
|
155
|
+
for (const [key, value] of entries) {
|
|
156
|
+
Map.prototype.set.call(this, key, value);
|
|
157
|
+
}
|
|
158
|
+
this.locked = true;
|
|
159
|
+
Object.freeze(this);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
override set(key: K, value: V): this {
|
|
163
|
+
if (this.locked) {
|
|
164
|
+
throw new TypeError("Agent Skill catalog is immutable.");
|
|
165
|
+
}
|
|
166
|
+
return super.set(key, value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
override delete(key: K): boolean {
|
|
170
|
+
if (this.locked) {
|
|
171
|
+
throw new TypeError("Agent Skill catalog is immutable.");
|
|
172
|
+
}
|
|
173
|
+
return super.delete(key);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
override clear(): void {
|
|
177
|
+
if (this.locked) {
|
|
178
|
+
throw new TypeError("Agent Skill catalog is immutable.");
|
|
179
|
+
}
|
|
180
|
+
super.clear();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { MessageId } from "../ids/runtime-id";
|
|
4
|
+
import { contentHash } from "../context/protocol-frame";
|
|
5
|
+
import { formatMessageSource } from "../context/context-source";
|
|
6
|
+
import type { SwapOverride } from "../context/context-revision";
|
|
7
|
+
import {
|
|
8
|
+
ACTIVE_SKILL_COUNT_MAX,
|
|
9
|
+
ACTIVE_SKILL_TOTAL_BYTES_MAX,
|
|
10
|
+
SkillError,
|
|
11
|
+
type LoadedSkill,
|
|
12
|
+
type SkillCatalogSnapshot,
|
|
13
|
+
} from "./skill-loader";
|
|
14
|
+
import {
|
|
15
|
+
activeSkillManifestEntry,
|
|
16
|
+
type ActiveSkillManifestEntry,
|
|
17
|
+
} from "./skill-catalog";
|
|
18
|
+
|
|
19
|
+
export const SKILL_ACTIVATION_RECEIPT_FORMAT = "skill-activation-receipt-v1" as const;
|
|
20
|
+
export const SKILL_POLICY_VERSION = "agent-skills-v1" as const;
|
|
21
|
+
|
|
22
|
+
export type ActiveSkill = {
|
|
23
|
+
readonly skill: LoadedSkill;
|
|
24
|
+
readonly activationMessageId: MessageId;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SkillActivationLifecycle = "reserved" | "pending" | "dispatched";
|
|
28
|
+
|
|
29
|
+
export class SkillActivationCoordinator {
|
|
30
|
+
private readonly active = new Map<string, ActiveSkill>();
|
|
31
|
+
private readonly unresolved = new Map<
|
|
32
|
+
string,
|
|
33
|
+
{ skill: LoadedSkill; lifecycle: SkillActivationLifecycle }
|
|
34
|
+
>();
|
|
35
|
+
|
|
36
|
+
constructor(input: { active?: readonly ActiveSkill[] } = {}) {
|
|
37
|
+
for (const entry of input.active ?? []) {
|
|
38
|
+
if (this.active.has(entry.skill.name)) {
|
|
39
|
+
throw new SkillError(
|
|
40
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
41
|
+
`Duplicate active Agent Skill ${entry.skill.name}.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
this.active.set(entry.skill.name, entry);
|
|
45
|
+
}
|
|
46
|
+
this.assertLimits();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
status(name: string):
|
|
50
|
+
| { status: "inactive" }
|
|
51
|
+
| { status: "active"; skill: LoadedSkill }
|
|
52
|
+
| {
|
|
53
|
+
status: "loaded";
|
|
54
|
+
skill: LoadedSkill;
|
|
55
|
+
lifecycle: "pending" | "dispatched";
|
|
56
|
+
} {
|
|
57
|
+
const active = this.active.get(name);
|
|
58
|
+
if (active !== undefined) {
|
|
59
|
+
return { status: "active", skill: active.skill };
|
|
60
|
+
}
|
|
61
|
+
const unresolved = this.unresolved.get(name);
|
|
62
|
+
if (unresolved === undefined) {
|
|
63
|
+
return { status: "inactive" };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
status: "loaded",
|
|
67
|
+
skill: unresolved.skill,
|
|
68
|
+
lifecycle: unresolved.lifecycle === "dispatched" ? "dispatched" : "pending",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
reserve(skill: LoadedSkill): void {
|
|
73
|
+
if (this.active.has(skill.name) || this.unresolved.has(skill.name)) {
|
|
74
|
+
throw new SkillError(
|
|
75
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
76
|
+
`Agent Skill ${skill.name} is already active or loaded.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
this.unresolved.set(skill.name, { skill, lifecycle: "reserved" });
|
|
80
|
+
try {
|
|
81
|
+
this.assertLimits();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.unresolved.delete(skill.name);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
release(name: string): void {
|
|
89
|
+
const entry = this.unresolved.get(name);
|
|
90
|
+
if (entry?.lifecycle === "reserved") {
|
|
91
|
+
this.unresolved.delete(name);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
markPending(name: string): void {
|
|
96
|
+
const entry = this.unresolved.get(name);
|
|
97
|
+
if (entry?.lifecycle !== "reserved") {
|
|
98
|
+
throw new SkillError(
|
|
99
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
100
|
+
`Agent Skill ${name} is not reserved for a pending activation.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
entry.lifecycle = "pending";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
markDispatched(names: readonly string[]): void {
|
|
107
|
+
if (new Set(names).size !== names.length) {
|
|
108
|
+
throw new SkillError(
|
|
109
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
110
|
+
"Agent Skill dispatch list contains duplicate names.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
const entry = this.unresolved.get(name);
|
|
115
|
+
if (entry?.lifecycle !== "pending") {
|
|
116
|
+
throw new SkillError(
|
|
117
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
118
|
+
`Agent Skill ${name} is not pending dispatch.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const name of names) {
|
|
123
|
+
this.unresolved.get(name)!.lifecycle = "dispatched";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
replaceActive(entries: readonly ActiveSkill[]): void {
|
|
128
|
+
validateActiveLimits(entries);
|
|
129
|
+
const next = new Map<string, ActiveSkill>();
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (next.has(entry.skill.name)) {
|
|
132
|
+
throw new SkillError(
|
|
133
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
134
|
+
`Duplicate active Agent Skill ${entry.skill.name}.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
next.set(entry.skill.name, entry);
|
|
138
|
+
}
|
|
139
|
+
this.active.clear();
|
|
140
|
+
for (const [name, entry] of next) {
|
|
141
|
+
this.active.set(name, entry);
|
|
142
|
+
this.unresolved.delete(entry.skill.name);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
settle(names: readonly string[]): void {
|
|
147
|
+
for (const name of names) {
|
|
148
|
+
this.unresolved.delete(name);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
activeEntries(): readonly ActiveSkill[] {
|
|
153
|
+
return Object.freeze(
|
|
154
|
+
[...this.active.values()].sort((left, right) =>
|
|
155
|
+
compareText(left.skill.name, right.skill.name),
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
activeManifest(): readonly ActiveSkillManifestEntry[] {
|
|
161
|
+
return Object.freeze(
|
|
162
|
+
this.activeEntries().map((entry) =>
|
|
163
|
+
activeSkillManifestEntry(entry.skill, entry.activationMessageId),
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private assertLimits(): void {
|
|
169
|
+
const skills = [
|
|
170
|
+
...[...this.active.values()].map((entry) => entry.skill),
|
|
171
|
+
...[...this.unresolved.values()].map((entry) => entry.skill),
|
|
172
|
+
];
|
|
173
|
+
if (skills.length > ACTIVE_SKILL_COUNT_MAX) {
|
|
174
|
+
throw new SkillError(
|
|
175
|
+
"SKILL_ACTIVE_LIMIT_EXCEEDED",
|
|
176
|
+
`Agent Skill activation count ${skills.length} exceeds the limit ${ACTIVE_SKILL_COUNT_MAX}.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const bytes = skills.reduce((total, skill) => total + skill.byteLength, 0);
|
|
180
|
+
if (bytes > ACTIVE_SKILL_TOTAL_BYTES_MAX) {
|
|
181
|
+
throw new SkillError(
|
|
182
|
+
"SKILL_ACTIVE_LIMIT_EXCEEDED",
|
|
183
|
+
`Active Agent Skill content is ${bytes} bytes; the limit is ${ACTIVE_SKILL_TOTAL_BYTES_MAX} bytes.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function buildActiveSystemPrompt(input: {
|
|
190
|
+
baseSystemPrompt: string;
|
|
191
|
+
activeSkills: readonly ActiveSkill[];
|
|
192
|
+
}): string {
|
|
193
|
+
if (input.baseSystemPrompt.trim() === "") {
|
|
194
|
+
throw new SkillError(
|
|
195
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
196
|
+
"Base system prompt must not be empty.",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (input.activeSkills.length === 0) {
|
|
200
|
+
return input.baseSystemPrompt;
|
|
201
|
+
}
|
|
202
|
+
const blocks = [...input.activeSkills]
|
|
203
|
+
.sort((left, right) => compareText(left.skill.name, right.skill.name))
|
|
204
|
+
.map(({ skill }) => renderActiveSkillBlock(skill));
|
|
205
|
+
return `${input.baseSystemPrompt}\n\n<active_agent_skills>
|
|
206
|
+
The following Agent Skills are active for this session.
|
|
207
|
+
Use each skill only when relevant to the current task.
|
|
208
|
+
They do not override Tinker's runtime, tool protocol, project instructions,
|
|
209
|
+
or the user's explicit request.
|
|
210
|
+
|
|
211
|
+
${blocks.join("\n\n")}
|
|
212
|
+
</active_agent_skills>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function rebindActiveSkills(input: {
|
|
216
|
+
catalog: SkillCatalogSnapshot;
|
|
217
|
+
active: readonly ActiveSkillManifestEntry[];
|
|
218
|
+
promotionNames?: readonly { name: string; activationMessageId: MessageId }[];
|
|
219
|
+
}): {
|
|
220
|
+
active: readonly ActiveSkill[];
|
|
221
|
+
refreshed: readonly string[];
|
|
222
|
+
deactivated: readonly string[];
|
|
223
|
+
} {
|
|
224
|
+
const currentByName = new Map<string, ActiveSkillManifestEntry>();
|
|
225
|
+
for (const entry of input.active) {
|
|
226
|
+
if (currentByName.has(entry.name)) {
|
|
227
|
+
throw new SkillError(
|
|
228
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
229
|
+
`Stored active Agent Skills contain duplicate name ${entry.name}.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
currentByName.set(entry.name, entry);
|
|
233
|
+
}
|
|
234
|
+
const active: ActiveSkill[] = [];
|
|
235
|
+
const refreshed: string[] = [];
|
|
236
|
+
const deactivated: string[] = [];
|
|
237
|
+
for (const stored of [...currentByName.values()].sort((left, right) =>
|
|
238
|
+
compareText(left.name, right.name),
|
|
239
|
+
)) {
|
|
240
|
+
const skill = input.catalog.skills.get(stored.name);
|
|
241
|
+
if (skill === undefined) {
|
|
242
|
+
deactivated.push(stored.name);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
active.push({ skill, activationMessageId: stored.activationMessageId });
|
|
246
|
+
if (
|
|
247
|
+
stored.scope !== skill.scope ||
|
|
248
|
+
stored.directorySha256 !== hashDirectory(skill.directory) ||
|
|
249
|
+
stored.descriptionSha256 !== contentDigest(skill.description) ||
|
|
250
|
+
stored.skillFileSha256 !== skill.sha256 ||
|
|
251
|
+
stored.byteLength !== skill.byteLength
|
|
252
|
+
) {
|
|
253
|
+
refreshed.push(skill.name);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const promotionNames = new Set<string>();
|
|
257
|
+
for (const promotion of [...(input.promotionNames ?? [])].sort((left, right) =>
|
|
258
|
+
compareText(left.name, right.name),
|
|
259
|
+
)) {
|
|
260
|
+
if (promotionNames.has(promotion.name) || currentByName.has(promotion.name)) {
|
|
261
|
+
throw new SkillError(
|
|
262
|
+
"SKILL_ACTIVATION_STATE_INVALID",
|
|
263
|
+
`Agent Skill ${promotion.name} has duplicate active or dispatched state.`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
promotionNames.add(promotion.name);
|
|
267
|
+
const skill = input.catalog.skills.get(promotion.name);
|
|
268
|
+
if (skill !== undefined) {
|
|
269
|
+
active.push({ skill, activationMessageId: promotion.activationMessageId });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
active.sort((left, right) => compareText(left.skill.name, right.skill.name));
|
|
273
|
+
validateActiveLimits(active);
|
|
274
|
+
return {
|
|
275
|
+
active: Object.freeze(active),
|
|
276
|
+
refreshed: Object.freeze(refreshed),
|
|
277
|
+
deactivated: Object.freeze(deactivated),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function renderSkillActivationReceipt(input: {
|
|
282
|
+
message: {
|
|
283
|
+
messageId: MessageId;
|
|
284
|
+
frameId: SwapOverride["frameId"];
|
|
285
|
+
ordinal: number;
|
|
286
|
+
content: string;
|
|
287
|
+
contentSha256: string;
|
|
288
|
+
};
|
|
289
|
+
name: string;
|
|
290
|
+
outcome: "promoted" | "rejected" | "unavailable";
|
|
291
|
+
}): SwapOverride {
|
|
292
|
+
const source = formatMessageSource(input.message.messageId);
|
|
293
|
+
const renderedContent =
|
|
294
|
+
input.outcome === "promoted"
|
|
295
|
+
? [
|
|
296
|
+
"[Tinker Agent Skill activation promoted]",
|
|
297
|
+
`name=${input.name}`,
|
|
298
|
+
`source=${source}`,
|
|
299
|
+
"activation=The full historical instructions were promoted out of this tool observation.",
|
|
300
|
+
"current=Consult the current active_agent_skills system section; absence means inactive.",
|
|
301
|
+
"historical=Use Recall get with source to recover the original activation observation.",
|
|
302
|
+
].join("\n")
|
|
303
|
+
: [
|
|
304
|
+
"[Tinker Agent Skill activation rejected]",
|
|
305
|
+
`name=${input.name}`,
|
|
306
|
+
`source=${source}`,
|
|
307
|
+
`status=${input.outcome === "unavailable" ? "unavailable" : "not_dispatched"}`,
|
|
308
|
+
"current=This skill was not added to the active system surface.",
|
|
309
|
+
"historical=Use Recall get with source to recover the original activation observation.",
|
|
310
|
+
].join("\n");
|
|
311
|
+
const originalBytes = Buffer.byteLength(input.message.content, "utf8");
|
|
312
|
+
const renderedBytes = Buffer.byteLength(renderedContent, "utf8");
|
|
313
|
+
return Object.freeze({
|
|
314
|
+
frameId: input.message.frameId,
|
|
315
|
+
messageId: input.message.messageId,
|
|
316
|
+
ordinal: input.message.ordinal,
|
|
317
|
+
source,
|
|
318
|
+
originalContentSha256: input.message.contentSha256,
|
|
319
|
+
renderedContent,
|
|
320
|
+
renderedContentSha256: contentHash(renderedContent),
|
|
321
|
+
originalBytes,
|
|
322
|
+
renderedBytes,
|
|
323
|
+
byteSavings: originalBytes - renderedBytes,
|
|
324
|
+
rendererFormat: SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function renderActiveSkillBlock(skill: LoadedSkill): string {
|
|
329
|
+
const content = skill.content + (skill.content.endsWith("\n") ? "" : "\n");
|
|
330
|
+
return `<agent_skill name=${JSON.stringify(skill.name)} scope=${JSON.stringify(skill.scope)}>
|
|
331
|
+
Skill directory: ${skill.directory}
|
|
332
|
+
Relative paths resolve from this directory.
|
|
333
|
+
<skill_file>
|
|
334
|
+
${content}</skill_file>
|
|
335
|
+
</agent_skill>`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function validateActiveLimits(active: readonly ActiveSkill[]): void {
|
|
339
|
+
if (active.length > ACTIVE_SKILL_COUNT_MAX) {
|
|
340
|
+
throw new SkillError(
|
|
341
|
+
"SKILL_ACTIVE_LIMIT_EXCEEDED",
|
|
342
|
+
`Active Agent Skill count ${active.length} exceeds the limit ${ACTIVE_SKILL_COUNT_MAX}.`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const bytes = active.reduce((total, entry) => total + entry.skill.byteLength, 0);
|
|
346
|
+
if (bytes > ACTIVE_SKILL_TOTAL_BYTES_MAX) {
|
|
347
|
+
throw new SkillError(
|
|
348
|
+
"SKILL_ACTIVE_LIMIT_EXCEEDED",
|
|
349
|
+
`Active Agent Skill content is ${bytes} bytes; the limit is ${ACTIVE_SKILL_TOTAL_BYTES_MAX} bytes.`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function hashDirectory(directory: string): string {
|
|
355
|
+
return contentDigest(path.resolve(directory));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function contentDigest(content: string): string {
|
|
359
|
+
return createHash("sha256").update(content).digest("hex");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function compareText(left: string, right: string): number {
|
|
363
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
364
|
+
}
|