@ai0x0/codex-ts 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/src/base_instructions.d.ts +20 -0
- package/dist/core/src/base_instructions.d.ts.map +1 -0
- package/dist/core/src/base_instructions.js +40 -0
- package/dist/core/src/base_instructions.js.map +1 -0
- package/dist/core/src/codex_thread.d.ts +48 -0
- package/dist/core/src/codex_thread.d.ts.map +1 -1
- package/dist/core/src/codex_thread.js +72 -6
- package/dist/core/src/codex_thread.js.map +1 -1
- package/dist/core/src/compact.d.ts +37 -0
- package/dist/core/src/compact.d.ts.map +1 -0
- package/dist/core/src/compact.js +154 -0
- package/dist/core/src/compact.js.map +1 -0
- package/dist/core/src/index.d.ts +6 -0
- package/dist/core/src/index.d.ts.map +1 -1
- package/dist/core/src/index.js +4 -0
- package/dist/core/src/index.js.map +1 -1
- package/dist/core/src/session/sse.d.ts +7 -0
- package/dist/core/src/session/sse.d.ts.map +1 -0
- package/dist/core/src/session/sse.js +40 -0
- package/dist/core/src/session/sse.js.map +1 -0
- package/dist/core/src/session/turn.d.ts +29 -1
- package/dist/core/src/session/turn.d.ts.map +1 -1
- package/dist/core/src/session/turn.js +90 -40
- package/dist/core/src/session/turn.js.map +1 -1
- package/dist/core/src/skills.d.ts +66 -0
- package/dist/core/src/skills.d.ts.map +1 -0
- package/dist/core/src/skills.js +304 -0
- package/dist/core/src/skills.js.map +1 -0
- package/dist/core/src/state/auto_compact_window.d.ts +42 -0
- package/dist/core/src/state/auto_compact_window.d.ts.map +1 -0
- package/dist/core/src/state/auto_compact_window.js +59 -0
- package/dist/core/src/state/auto_compact_window.js.map +1 -0
- package/dist/core/src/tools/router.d.ts +26 -1
- package/dist/core/src/tools/router.d.ts.map +1 -1
- package/dist/core/src/tools/router.js +15 -2
- package/dist/core/src/tools/router.js.map +1 -1
- package/dist/core/tests/suite/compact.test.d.ts +10 -0
- package/dist/core/tests/suite/compact.test.d.ts.map +1 -0
- package/dist/core/tests/suite/compact.test.js +170 -0
- package/dist/core/tests/suite/compact.test.js.map +1 -0
- package/dist/core/tests/suite/extensions.test.d.ts +8 -0
- package/dist/core/tests/suite/extensions.test.d.ts.map +1 -0
- package/dist/core/tests/suite/extensions.test.js +186 -0
- package/dist/core/tests/suite/extensions.test.js.map +1 -0
- package/dist/core/tests/suite/message_item_type.test.d.ts +17 -0
- package/dist/core/tests/suite/message_item_type.test.d.ts.map +1 -0
- package/dist/core/tests/suite/message_item_type.test.js +111 -0
- package/dist/core/tests/suite/message_item_type.test.js.map +1 -0
- package/dist/core/tests/suite/skills.test.d.ts +7 -0
- package/dist/core/tests/suite/skills.test.d.ts.map +1 -0
- package/dist/core/tests/suite/skills.test.js +324 -0
- package/dist/core/tests/suite/skills.test.js.map +1 -0
- package/dist/protocol/src/index.d.ts +1 -0
- package/dist/protocol/src/index.d.ts.map +1 -1
- package/dist/protocol/src/protocol.d.ts +32 -0
- package/dist/protocol/src/protocol.d.ts.map +1 -1
- package/dist/thread-store/src/index.d.ts +1 -0
- package/dist/thread-store/src/index.d.ts.map +1 -1
- package/dist/thread-store/src/index.js +1 -0
- package/dist/thread-store/src/index.js.map +1 -1
- package/dist/thread-store/src/indexeddb.d.ts +29 -0
- package/dist/thread-store/src/indexeddb.d.ts.map +1 -0
- package/dist/thread-store/src/indexeddb.js +97 -0
- package/dist/thread-store/src/indexeddb.js.map +1 -0
- package/dist/thread-store/src/types.d.ts +2 -0
- package/dist/thread-store/src/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { EventMsg } from "../../../protocol/src/protocol.js";
|
|
13
13
|
import type { UserInput } from "../../../protocol/src/user_input.js";
|
|
14
14
|
import { ToolRouter } from "../tools/router.js";
|
|
15
|
+
import type { SkillMetadata } from "../skills.js";
|
|
15
16
|
import type { PendingInputs } from "../tools/handlers/request_user_input.js";
|
|
16
17
|
import type { LiveThread } from "../../../thread-store/src/live_thread.js";
|
|
17
18
|
import type { ConversationItem } from "../../../thread-store/src/types.js";
|
|
@@ -21,13 +22,40 @@ export interface TurnConfig {
|
|
|
21
22
|
baseUrl: string;
|
|
22
23
|
model: string;
|
|
23
24
|
instructions?: string | undefined;
|
|
25
|
+
/** Discovered skills, for `$skill-name` full-body injection (Layer 2). */
|
|
26
|
+
skills?: SkillMetadata[] | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Reads a skill's full SKILL.md on demand. Host-provided because a browser
|
|
29
|
+
* has no filesystem (mirrors how IoBackend is injected).
|
|
30
|
+
*/
|
|
31
|
+
loadSkillContent?: ((skill: SkillMetadata) => Promise<string>) | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Input-token threshold that triggers inline auto-compaction.
|
|
34
|
+
* mirrors model_auto_compact_token_limit in codex-rs TurnContext config.
|
|
35
|
+
*
|
|
36
|
+
* In BodyAfterPrefix mode (the default) this is compared against tokens
|
|
37
|
+
* added since the last compaction, not total context size.
|
|
38
|
+
* Typical value: context_window × 0.9.
|
|
39
|
+
* Omit to disable auto-compaction.
|
|
40
|
+
*/
|
|
41
|
+
autoCompactTokenLimit?: number | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Turn-scoped context messages prepended to `input` ahead of history,
|
|
44
|
+
* mirroring codex-rs's contextual user fragments: user_instructions
|
|
45
|
+
* (developer + AGENTS.md) and the skills catalog ride here as discrete
|
|
46
|
+
* messages rather than baked into the `instructions` field. Not persisted.
|
|
47
|
+
*/
|
|
48
|
+
contextItems?: HistoryItem[] | undefined;
|
|
24
49
|
}
|
|
25
50
|
export interface TurnResult {
|
|
26
51
|
lastAgentMessage: string;
|
|
27
52
|
/** All events emitted during the turn (including streaming deltas) */
|
|
28
53
|
events: EventMsg[];
|
|
29
54
|
}
|
|
30
|
-
export declare function runTurn(turnId: string, userItems: UserInput[], history: HistoryItem[], config: TurnConfig, router: ToolRouter, pendingInputs: PendingInputs, emitEvent: (msg: EventMsg) => void, liveThread?: LiveThread | undefined
|
|
55
|
+
export declare function runTurn(turnId: string, userItems: UserInput[], history: HistoryItem[], config: TurnConfig, router: ToolRouter, pendingInputs: PendingInputs, emitEvent: (msg: EventMsg) => void, liveThread?: LiveThread | undefined,
|
|
56
|
+
/** mirrors: codex-rs propagates a tokio CancellationToken into the turn loop;
|
|
57
|
+
* AbortSignal is the browser-native equivalent. */
|
|
58
|
+
abortSignal?: AbortSignal | undefined): Promise<{
|
|
31
59
|
lastAgentMessage: string;
|
|
32
60
|
}>;
|
|
33
61
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../../../core/src/session/turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AAErE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../../../core/src/session/turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AAErE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAC3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAO3E,KAAK,WAAW,GAAG,gBAAgB,CAAC;AAIpC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,aAAa,EAAE,GAAG,SAAS,CAAC;IACrC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAC3E;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C;;;;;OAKG;IACH,YAAY,CAAC,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;CAC1C;AAID,MAAM,WAAW,UAAU;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,MAAM,EAAE,QAAQ,EAAE,CAAC;CACpB;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EAAE,EACtB,OAAO,EAAE,WAAW,EAAE,EACtB,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,aAAa,EAC5B,SAAS,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,IAAI,EAClC,UAAU,CAAC,EAAE,UAAU,GAAG,SAAS;AACnC;oDACoD;AACpD,WAAW,CAAC,EAAE,WAAW,GAAG,SAAS,GACpC,OAAO,CAAC;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAC,CA8NvC"}
|
|
@@ -10,60 +10,75 @@
|
|
|
10
10
|
* 6. Loop until the model produces no more tool calls
|
|
11
11
|
*/
|
|
12
12
|
import { toolSpecToRequestJson } from "../../../tools/src/tool_spec.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (done)
|
|
22
|
-
break;
|
|
23
|
-
buffer += decoder.decode(value, { stream: true });
|
|
24
|
-
const lines = buffer.split("\n");
|
|
25
|
-
buffer = lines.pop() ?? "";
|
|
26
|
-
let dataLine = "";
|
|
27
|
-
for (const line of lines) {
|
|
28
|
-
if (line.startsWith("data: ")) {
|
|
29
|
-
dataLine = line.slice(6).trim();
|
|
30
|
-
}
|
|
31
|
-
else if (line === "" && dataLine) {
|
|
32
|
-
if (dataLine !== "[DONE]") {
|
|
33
|
-
try {
|
|
34
|
-
yield JSON.parse(dataLine);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
/* malformed JSON — skip */
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
dataLine = "";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
reader.releaseLock();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
export async function runTurn(turnId, userItems, history, config, router, pendingInputs, emitEvent, liveThread) {
|
|
13
|
+
import { extractSkillMentions, renderSkillInjection } from "../skills.js";
|
|
14
|
+
import { parseSseStream } from "./sse.js";
|
|
15
|
+
import { runInlineAutoCompactTask } from "../compact.js";
|
|
16
|
+
import { AutoCompactWindow } from "../state/auto_compact_window.js";
|
|
17
|
+
export async function runTurn(turnId, userItems, history, config, router, pendingInputs, emitEvent, liveThread,
|
|
18
|
+
/** mirrors: codex-rs propagates a tokio CancellationToken into the turn loop;
|
|
19
|
+
* AbortSignal is the browser-native equivalent. */
|
|
20
|
+
abortSignal) {
|
|
50
21
|
// Add user message to history (mutates the shared array)
|
|
51
22
|
const userContent = userItems
|
|
52
23
|
.map((item) => item.type === "text"
|
|
53
24
|
? { type: "input_text", text: item.text }
|
|
54
25
|
: null)
|
|
55
26
|
.filter((x) => x !== null);
|
|
56
|
-
const userMsg = {
|
|
27
|
+
const userMsg = {
|
|
28
|
+
type: "message",
|
|
29
|
+
role: "user",
|
|
30
|
+
content: userContent,
|
|
31
|
+
};
|
|
57
32
|
history.push(userMsg);
|
|
58
33
|
await liveThread?.appendConversationItems([userMsg]);
|
|
34
|
+
// ── Layer 2: turn-scoped skill full-body injection (mirrors codex-rs) ──────
|
|
35
|
+
// A `$skill-name` mention in user input pulls that skill's full SKILL.md into
|
|
36
|
+
// THIS turn's request input only. Like codex-rs contextual fragments, these
|
|
37
|
+
// items are NOT pushed to history / persisted — resume stays clean and the
|
|
38
|
+
// body isn't duplicated every turn.
|
|
39
|
+
const skillInjectionItems = [];
|
|
40
|
+
const loadSkillContent = config.loadSkillContent;
|
|
41
|
+
if (config.skills && config.skills.length > 0 && loadSkillContent) {
|
|
42
|
+
const userText = userItems
|
|
43
|
+
.map((item) => (item.type === "text" ? item.text : ""))
|
|
44
|
+
.join("\n");
|
|
45
|
+
const mentioned = extractSkillMentions(userText, config.skills);
|
|
46
|
+
for (const skill of mentioned) {
|
|
47
|
+
const contents = await loadSkillContent(skill).catch(() => null);
|
|
48
|
+
if (contents !== null) {
|
|
49
|
+
skillInjectionItems.push({
|
|
50
|
+
type: "message",
|
|
51
|
+
role: "user",
|
|
52
|
+
content: [
|
|
53
|
+
{ type: "input_text", text: renderSkillInjection(skill, contents) },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
59
|
const tools = router.toolSpecs().map(toolSpecToRequestJson);
|
|
60
60
|
let lastAgentMessage = "";
|
|
61
61
|
let itemIdCounter = 0;
|
|
62
|
+
// ── Auto-compaction window tracker (mirrors AutoCompactWindow in codex-rs) ──
|
|
63
|
+
// Uses BodyAfterPrefix mode: only tokens added after the last compaction count
|
|
64
|
+
// toward the limit. mirrors AutoCompactTokenLimitScope::BodyAfterPrefix.
|
|
65
|
+
const compactWindow = new AutoCompactWindow();
|
|
62
66
|
for (;;) {
|
|
67
|
+
// Bail out before sampling again if the turn was interrupted.
|
|
68
|
+
if (abortSignal?.aborted) {
|
|
69
|
+
throw new DOMException("Turn interrupted", "AbortError");
|
|
70
|
+
}
|
|
63
71
|
// ── Sample from the model ───────────────────────────────────────────────
|
|
64
72
|
const body = {
|
|
65
73
|
model: config.model,
|
|
66
|
-
|
|
74
|
+
// codex-rs structure: turn-scoped context (user_instructions + skills
|
|
75
|
+
// catalog) and any $mention skill bodies ride at the front, ahead of the
|
|
76
|
+
// persisted conversation history. None of these are persisted.
|
|
77
|
+
input: [
|
|
78
|
+
...(config.contextItems ?? []),
|
|
79
|
+
...skillInjectionItems,
|
|
80
|
+
...history,
|
|
81
|
+
],
|
|
67
82
|
tools,
|
|
68
83
|
stream: true,
|
|
69
84
|
};
|
|
@@ -76,6 +91,7 @@ export async function runTurn(turnId, userItems, history, config, router, pendin
|
|
|
76
91
|
Authorization: `Bearer ${config.apiKey}`,
|
|
77
92
|
},
|
|
78
93
|
body: JSON.stringify(body),
|
|
94
|
+
signal: abortSignal ?? null,
|
|
79
95
|
});
|
|
80
96
|
if (!res.ok || !res.body) {
|
|
81
97
|
const text = res.body ? await res.text() : "(no body)";
|
|
@@ -86,6 +102,7 @@ export async function runTurn(turnId, userItems, history, config, router, pendin
|
|
|
86
102
|
const partialArgs = new Map();
|
|
87
103
|
let assistantText = "";
|
|
88
104
|
let currentItemId = `item-${++itemIdCounter}`;
|
|
105
|
+
let inputTokensThisRound = 0;
|
|
89
106
|
for await (const raw of parseSseStream(res.body)) {
|
|
90
107
|
switch (raw["type"]) {
|
|
91
108
|
case "response.output_item.added": {
|
|
@@ -125,12 +142,26 @@ export async function runTurn(turnId, userItems, history, config, router, pendin
|
|
|
125
142
|
}
|
|
126
143
|
break;
|
|
127
144
|
}
|
|
145
|
+
case "response.done": {
|
|
146
|
+
// mirrors ensure_server_observed_prefill_from_usage in auto_compact_window.rs
|
|
147
|
+
const resp = raw["response"];
|
|
148
|
+
const usage = resp?.["usage"];
|
|
149
|
+
if (typeof usage?.["input_tokens"] === "number") {
|
|
150
|
+
inputTokensThisRound = usage["input_tokens"];
|
|
151
|
+
compactWindow.ensureServerObservedPrefill(inputTokensThisRound);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
128
155
|
}
|
|
129
156
|
}
|
|
130
157
|
// ── Persist assistant message ────────────────────────────────────────────
|
|
131
158
|
if (assistantText) {
|
|
132
159
|
lastAgentMessage = assistantText;
|
|
133
|
-
const assistantItem = {
|
|
160
|
+
const assistantItem = {
|
|
161
|
+
type: "message",
|
|
162
|
+
role: "assistant",
|
|
163
|
+
content: assistantText,
|
|
164
|
+
};
|
|
134
165
|
history.push(assistantItem);
|
|
135
166
|
await liveThread?.appendConversationItems([assistantItem]);
|
|
136
167
|
emitEvent({ type: "AgentMessage", event: { message: assistantText } });
|
|
@@ -150,6 +181,25 @@ export async function runTurn(turnId, userItems, history, config, router, pendin
|
|
|
150
181
|
// ── Done when no tool calls ─────────────────────────────────────────────
|
|
151
182
|
if (functionCalls.length === 0)
|
|
152
183
|
break;
|
|
184
|
+
// ── Auto-compaction check (mirrors post-sampling compact in turn.rs) ─────
|
|
185
|
+
// Only triggered mid-turn (when there are more tool calls to dispatch),
|
|
186
|
+
// matching InitialContextInjection::BeforeLastUserMessage behaviour.
|
|
187
|
+
if (config.autoCompactTokenLimit !== undefined && inputTokensThisRound > 0) {
|
|
188
|
+
const scopeTokens = compactWindow.bodyAfterPrefix(inputTokensThisRound);
|
|
189
|
+
const limitReached = scopeTokens >= config.autoCompactTokenLimit;
|
|
190
|
+
if (limitReached) {
|
|
191
|
+
emitEvent({ type: "ContextCompacted", event: {} });
|
|
192
|
+
await runInlineAutoCompactTask(history, config);
|
|
193
|
+
compactWindow.startNext();
|
|
194
|
+
emitEvent({
|
|
195
|
+
type: "Warning",
|
|
196
|
+
event: {
|
|
197
|
+
message: "Heads up: Long threads and multiple compactions can cause the model to be less accurate. " +
|
|
198
|
+
"Start a new thread when possible to keep threads small and targeted.",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
153
203
|
// ── Dispatch tool calls ─────────────────────────────────────────────────
|
|
154
204
|
for (const call of functionCalls) {
|
|
155
205
|
let args;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn.js","sourceRoot":"","sources":["../../../../core/src/session/turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"turn.js","sourceRoot":"","sources":["../../../../core/src/session/turn.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mirrors codex-rs/core-skills/src/render.rs + injection.rs + skill_instructions.rs
|
|
3
|
+
*
|
|
4
|
+
* Two-layer skill injection — the layer codex-ts originally dropped, so the
|
|
5
|
+
* model had to grope around with list_dir/read_file every turn:
|
|
6
|
+
* - Layer 1 (catalog): renderSkillsCatalog() builds the always-on `## Skills`
|
|
7
|
+
* index, budget-trimmed so it never blows the context window.
|
|
8
|
+
* - Layer 2 (full body): extractSkillMentions() finds `$skill-name` mentions in
|
|
9
|
+
* the user's input so the host can inject the full SKILL.md on demand, and
|
|
10
|
+
* renderSkillInjection() wraps it in the codex-rs `<skill>` envelope.
|
|
11
|
+
*
|
|
12
|
+
* Discovery (scanning `.agents/skills` + parsing SKILL.md frontmatter) is the
|
|
13
|
+
* HOST's job — a browser has no filesystem — so the host passes already-found
|
|
14
|
+
* SkillMetadata[] plus a loadSkillContent() reader. This mirrors how codex-ts
|
|
15
|
+
* already injects IoBackend/ThreadStore from the environment. The codex-rs
|
|
16
|
+
* skill-roots/alias variant is omitted: a single `.agents/skills` root means
|
|
17
|
+
* plain absolute/relative paths, never aliases.
|
|
18
|
+
*/
|
|
19
|
+
export interface SkillMetadata {
|
|
20
|
+
/** Skill name from SKILL.md frontmatter (e.g. "song-analyzer"). */
|
|
21
|
+
name: string;
|
|
22
|
+
/** One-line description: what it does + when to use it. */
|
|
23
|
+
description: string;
|
|
24
|
+
/** Path to the SKILL.md, shown in the catalog and read on demand. */
|
|
25
|
+
path: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Context budget for the skills catalog. Tokens are approximated as bytes/4
|
|
29
|
+
* (codex-rs APPROX_BYTES_PER_TOKEN); characters count Unicode code points.
|
|
30
|
+
*/
|
|
31
|
+
export type SkillMetadataBudget = {
|
|
32
|
+
kind: "tokens";
|
|
33
|
+
limit: number;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "characters";
|
|
36
|
+
limit: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Default budget: 2% of the model's context window in tokens when known,
|
|
40
|
+
* otherwise a flat 8000-character cap. Mirrors render.rs default_skill_metadata_budget.
|
|
41
|
+
*/
|
|
42
|
+
export declare function defaultSkillMetadataBudget(contextWindow?: number): SkillMetadataBudget;
|
|
43
|
+
/**
|
|
44
|
+
* Render the always-on skills catalog (Layer 1), trimmed to fit `budget`.
|
|
45
|
+
* Mirrors render.rs render_available_skills_body (absolute-paths variant)
|
|
46
|
+
* including the omission / truncation warning appended when the budget is tight.
|
|
47
|
+
* Returns "" when there are no skills so callers can skip injection entirely.
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderSkillsCatalog(skills: SkillMetadata[], budget?: SkillMetadataBudget): string;
|
|
50
|
+
/**
|
|
51
|
+
* Extract `$skill-name` mentions from text (Layer 2).
|
|
52
|
+
* Mirrors injection.rs extract_tool_mentions + select_skills_from_mentions:
|
|
53
|
+
* a plain `$name` only resolves when exactly one skill carries that name.
|
|
54
|
+
* Results preserve `skills` order and are de-duplicated.
|
|
55
|
+
*
|
|
56
|
+
* The codex-rs `[$name](path)` linked form and structured `UserInput::Skill`
|
|
57
|
+
* selection are intentionally not mirrored — ace-mv mentions skills by plain
|
|
58
|
+
* name only.
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractSkillMentions(text: string, skills: SkillMetadata[]): SkillMetadata[];
|
|
61
|
+
/**
|
|
62
|
+
* Wrap a skill's full SKILL.md body for injection as a turn-scoped user message.
|
|
63
|
+
* Mirrors skill_instructions.rs SkillInstructions::body + the `<skill>` markers.
|
|
64
|
+
*/
|
|
65
|
+
export declare function renderSkillInjection(skill: SkillMetadata, contents: string): string;
|
|
66
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../../core/src/skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;CACd;AAID;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAmC1C;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,aAAa,CAAC,EAAE,MAAM,GACrB,mBAAmB,CASrB;AAsOD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,aAAa,EAAE,EACvB,MAAM,GAAE,mBAAkD,GACzD,MAAM,CAyBR;AAqBD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,EAAE,GACtB,aAAa,EAAE,CAuBjB;AAiBD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,MAAM,GACf,MAAM,CAER"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mirrors codex-rs/core-skills/src/render.rs + injection.rs + skill_instructions.rs
|
|
3
|
+
*
|
|
4
|
+
* Two-layer skill injection — the layer codex-ts originally dropped, so the
|
|
5
|
+
* model had to grope around with list_dir/read_file every turn:
|
|
6
|
+
* - Layer 1 (catalog): renderSkillsCatalog() builds the always-on `## Skills`
|
|
7
|
+
* index, budget-trimmed so it never blows the context window.
|
|
8
|
+
* - Layer 2 (full body): extractSkillMentions() finds `$skill-name` mentions in
|
|
9
|
+
* the user's input so the host can inject the full SKILL.md on demand, and
|
|
10
|
+
* renderSkillInjection() wraps it in the codex-rs `<skill>` envelope.
|
|
11
|
+
*
|
|
12
|
+
* Discovery (scanning `.agents/skills` + parsing SKILL.md frontmatter) is the
|
|
13
|
+
* HOST's job — a browser has no filesystem — so the host passes already-found
|
|
14
|
+
* SkillMetadata[] plus a loadSkillContent() reader. This mirrors how codex-ts
|
|
15
|
+
* already injects IoBackend/ThreadStore from the environment. The codex-rs
|
|
16
|
+
* skill-roots/alias variant is omitted: a single `.agents/skills` root means
|
|
17
|
+
* plain absolute/relative paths, never aliases.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_SKILL_METADATA_CHAR_BUDGET = 8_000;
|
|
20
|
+
const SKILL_METADATA_CONTEXT_WINDOW_PERCENT = 2;
|
|
21
|
+
const APPROX_BYTES_PER_TOKEN = 4;
|
|
22
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
23
|
+
function byteLength(text) {
|
|
24
|
+
return TEXT_ENCODER.encode(text).length;
|
|
25
|
+
}
|
|
26
|
+
function approxTokensFromBytes(bytes) {
|
|
27
|
+
return Math.floor((bytes + APPROX_BYTES_PER_TOKEN - 1) / APPROX_BYTES_PER_TOKEN);
|
|
28
|
+
}
|
|
29
|
+
function budgetCost(budget, text) {
|
|
30
|
+
return budget.kind === "tokens"
|
|
31
|
+
? approxTokensFromBytes(byteLength(text))
|
|
32
|
+
: Array.from(text).length;
|
|
33
|
+
}
|
|
34
|
+
function budgetCostFromCounts(budget, chars, bytes) {
|
|
35
|
+
return budget.kind === "tokens" ? approxTokensFromBytes(bytes) : chars;
|
|
36
|
+
}
|
|
37
|
+
// Each rendered line is followed by a newline in the catalog body.
|
|
38
|
+
function lineCost(budget, line) {
|
|
39
|
+
return budgetCost(budget, `${line}\n`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Default budget: 2% of the model's context window in tokens when known,
|
|
43
|
+
* otherwise a flat 8000-character cap. Mirrors render.rs default_skill_metadata_budget.
|
|
44
|
+
*/
|
|
45
|
+
export function defaultSkillMetadataBudget(contextWindow) {
|
|
46
|
+
if (contextWindow && contextWindow > 0) {
|
|
47
|
+
const limit = Math.max(1, Math.floor((contextWindow * SKILL_METADATA_CONTEXT_WINDOW_PERCENT) / 100));
|
|
48
|
+
return { kind: "tokens", limit };
|
|
49
|
+
}
|
|
50
|
+
return { kind: "characters", limit: DEFAULT_SKILL_METADATA_CHAR_BUDGET };
|
|
51
|
+
}
|
|
52
|
+
// ─── Layer 1: line rendering (mirrors render.rs SkillLine) ────────────────────
|
|
53
|
+
function normalizedPath(skill) {
|
|
54
|
+
return skill.path.replace(/\\/g, "/");
|
|
55
|
+
}
|
|
56
|
+
// render.rs SkillLine::render_full
|
|
57
|
+
function renderSkillLineFull(skill) {
|
|
58
|
+
return renderSkillLineWithDescriptionChars(skill, Array.from(skill.description).length);
|
|
59
|
+
}
|
|
60
|
+
// render.rs SkillLine::render_with_description_chars
|
|
61
|
+
function renderSkillLineWithDescriptionChars(skill, descriptionChars) {
|
|
62
|
+
const path = normalizedPath(skill);
|
|
63
|
+
if (descriptionChars === 0) {
|
|
64
|
+
return `- ${skill.name}: (file: ${path})`;
|
|
65
|
+
}
|
|
66
|
+
const description = Array.from(skill.description)
|
|
67
|
+
.slice(0, descriptionChars)
|
|
68
|
+
.join("");
|
|
69
|
+
return `- ${skill.name}: ${description} (file: ${path})`;
|
|
70
|
+
}
|
|
71
|
+
function descriptionBudgetLine(skill, budget) {
|
|
72
|
+
const minimumLine = renderSkillLineWithDescriptionChars(skill, /*descriptionChars*/ 0);
|
|
73
|
+
const minimumChars = Array.from(minimumLine).length + 1; // +1 for "\n"
|
|
74
|
+
const minimumBytes = byteLength(minimumLine) + 1;
|
|
75
|
+
const minimumCost = budgetCostFromCounts(budget, minimumChars, minimumBytes);
|
|
76
|
+
const descriptionCodePoints = Array.from(skill.description);
|
|
77
|
+
const extraCosts = [0];
|
|
78
|
+
let prefixChars = 0;
|
|
79
|
+
let prefixBytes = 0;
|
|
80
|
+
for (const ch of descriptionCodePoints) {
|
|
81
|
+
prefixChars += 1;
|
|
82
|
+
prefixBytes += byteLength(ch);
|
|
83
|
+
// A k-char description adds the chars plus one separating space.
|
|
84
|
+
const renderedChars = minimumChars + prefixChars + 1;
|
|
85
|
+
const renderedBytes = minimumBytes + prefixBytes + 1;
|
|
86
|
+
const cost = budgetCostFromCounts(budget, renderedChars, renderedBytes) - minimumCost;
|
|
87
|
+
extraCosts.push(Math.max(0, cost));
|
|
88
|
+
}
|
|
89
|
+
return { skill, descriptionCharCount: descriptionCodePoints.length, extraCosts };
|
|
90
|
+
}
|
|
91
|
+
// render.rs render_lines_with_description_budget — hand out description space one
|
|
92
|
+
// char at a time across all skills so a short description's slack flows to longer
|
|
93
|
+
// ones instead of being stranded in a fixed per-skill quota.
|
|
94
|
+
function renderLinesWithDescriptionBudget(skills, budget, limit) {
|
|
95
|
+
const budgetLines = skills.map((skill) => descriptionBudgetLine(skill, budget));
|
|
96
|
+
const charAllocations = budgetLines.map(() => 0);
|
|
97
|
+
const currentExtraCosts = budgetLines.map(() => 0);
|
|
98
|
+
let remaining = limit;
|
|
99
|
+
for (;;) {
|
|
100
|
+
let changed = false;
|
|
101
|
+
budgetLines.forEach((line, index) => {
|
|
102
|
+
if (charAllocations[index] >= line.descriptionCharCount) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const nextChars = charAllocations[index] + 1;
|
|
106
|
+
const nextCost = line.extraCosts[nextChars];
|
|
107
|
+
const delta = nextCost - currentExtraCosts[index];
|
|
108
|
+
if (delta <= remaining) {
|
|
109
|
+
charAllocations[index] = nextChars;
|
|
110
|
+
currentExtraCosts[index] = nextCost;
|
|
111
|
+
remaining -= delta;
|
|
112
|
+
changed = true;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
if (!changed) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return budgetLines.map((line, index) => renderSkillLineWithDescriptionChars(line.skill, charAllocations[index]));
|
|
120
|
+
}
|
|
121
|
+
// render.rs render_minimum_skill_lines_until_budget — when even the no-description
|
|
122
|
+
// lines overflow, keep adding minimum lines until the budget is spent; the rest
|
|
123
|
+
// are omitted.
|
|
124
|
+
function renderMinimumUntilBudget(skills, budget) {
|
|
125
|
+
const lines = [];
|
|
126
|
+
let used = 0;
|
|
127
|
+
let omitted = 0;
|
|
128
|
+
for (const skill of skills) {
|
|
129
|
+
const minimumLine = renderSkillLineWithDescriptionChars(skill, /*descriptionChars*/ 0);
|
|
130
|
+
const cost = lineCost(budget, minimumLine);
|
|
131
|
+
if (used + cost <= budget.limit) {
|
|
132
|
+
used += cost;
|
|
133
|
+
lines.push(minimumLine);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
omitted += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { lines, omitted };
|
|
140
|
+
}
|
|
141
|
+
// render.rs render_skill_lines_from_lines — three tiers: full → trimmed
|
|
142
|
+
// descriptions → minimum-until-budget.
|
|
143
|
+
function renderSkillLines(skills, budget) {
|
|
144
|
+
const fullCost = skills.reduce((used, skill) => used + lineCost(budget, renderSkillLineFull(skill)), 0);
|
|
145
|
+
if (fullCost <= budget.limit) {
|
|
146
|
+
return { lines: skills.map(renderSkillLineFull), omitted: 0 };
|
|
147
|
+
}
|
|
148
|
+
const minimumCost = skills.reduce((used, skill) => used + lineCost(budget, renderSkillLineWithDescriptionChars(skill, /*descriptionChars*/ 0)), 0);
|
|
149
|
+
if (minimumCost <= budget.limit) {
|
|
150
|
+
return {
|
|
151
|
+
lines: renderLinesWithDescriptionBudget(skills, budget, budget.limit - minimumCost),
|
|
152
|
+
omitted: 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return renderMinimumUntilBudget(skills, budget);
|
|
156
|
+
}
|
|
157
|
+
// ─── Layer 1: catalog (mirrors render.rs render_available_skills_body) ────────
|
|
158
|
+
// render.rs SKILLS_INTRO_WITH_ABSOLUTE_PATHS (verbatim)
|
|
159
|
+
const SKILLS_INTRO = "A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.";
|
|
160
|
+
// render.rs SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS (verbatim)
|
|
161
|
+
const SKILLS_HOW_TO_USE = `- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
|
|
162
|
+
- Trigger rules: If the user names a skill (with \`$SkillName\` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
|
|
163
|
+
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
|
|
164
|
+
- How to use a skill (progressive disclosure):
|
|
165
|
+
1) After deciding to use a skill, open its \`SKILL.md\`. Read only enough to follow the workflow.
|
|
166
|
+
2) When \`SKILL.md\` references relative paths (e.g., \`scripts/foo.py\`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
|
|
167
|
+
3) If \`SKILL.md\` points to extra folders such as \`references/\`, load only the specific files needed for the request; don't bulk-load everything.
|
|
168
|
+
4) If \`scripts/\` exist, prefer running or patching them instead of retyping large code blocks.
|
|
169
|
+
5) If \`assets/\` or templates exist, reuse them instead of recreating from scratch.
|
|
170
|
+
- Coordination and sequencing:
|
|
171
|
+
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
|
172
|
+
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
|
173
|
+
- Context hygiene:
|
|
174
|
+
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
|
|
175
|
+
- Avoid deep reference-chasing: prefer opening only files directly linked from \`SKILL.md\` unless you're blocked.
|
|
176
|
+
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
|
177
|
+
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.`;
|
|
178
|
+
// render.rs warning constants (verbatim)
|
|
179
|
+
const SKILL_DESCRIPTIONS_REMOVED_WARNING_PREFIX = "Exceeded skills context budget. All skill descriptions were removed and";
|
|
180
|
+
const SKILL_DESCRIPTION_TRUNCATED_WARNING = "Skill descriptions were shortened to fit the skills context budget. " +
|
|
181
|
+
"Codex can still see every skill, but some descriptions are shorter. " +
|
|
182
|
+
"Disable unused skills or plugins to leave more room for the rest.";
|
|
183
|
+
const SKILL_DESCRIPTION_TRUNCATED_WARNING_WITH_PERCENT = "Skill descriptions were shortened to fit the 2% skills context budget. " +
|
|
184
|
+
"Codex can still see every skill, but some descriptions are shorter. " +
|
|
185
|
+
"Disable unused skills or plugins to leave more room for the rest.";
|
|
186
|
+
/**
|
|
187
|
+
* Build the omission / truncation warning appended to the catalog.
|
|
188
|
+
* mirrors render.rs build_available_skills_from_lines warning_message logic.
|
|
189
|
+
*/
|
|
190
|
+
function buildCatalogWarning(omitted, truncated, budget) {
|
|
191
|
+
if (omitted > 0) {
|
|
192
|
+
const skillWord = omitted === 1 ? "skill" : "skills";
|
|
193
|
+
const verb = omitted === 1 ? "was" : "were";
|
|
194
|
+
const prefix = budget.kind === "tokens"
|
|
195
|
+
? SKILL_DESCRIPTIONS_REMOVED_WARNING_PREFIX.replace("skills context budget", "2% skills context budget")
|
|
196
|
+
: SKILL_DESCRIPTIONS_REMOVED_WARNING_PREFIX;
|
|
197
|
+
return `${prefix} ${omitted} additional ${skillWord} ${verb} not included in the model-visible skills list.`;
|
|
198
|
+
}
|
|
199
|
+
if (truncated) {
|
|
200
|
+
return budget.kind === "tokens"
|
|
201
|
+
? SKILL_DESCRIPTION_TRUNCATED_WARNING_WITH_PERCENT
|
|
202
|
+
: SKILL_DESCRIPTION_TRUNCATED_WARNING;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Render the always-on skills catalog (Layer 1), trimmed to fit `budget`.
|
|
208
|
+
* Mirrors render.rs render_available_skills_body (absolute-paths variant)
|
|
209
|
+
* including the omission / truncation warning appended when the budget is tight.
|
|
210
|
+
* Returns "" when there are no skills so callers can skip injection entirely.
|
|
211
|
+
*/
|
|
212
|
+
export function renderSkillsCatalog(skills, budget = defaultSkillMetadataBudget()) {
|
|
213
|
+
if (skills.length === 0) {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
const { lines, omitted } = renderSkillLines(skills, budget);
|
|
217
|
+
// Detect whether any description was truncated by comparing rendered lengths
|
|
218
|
+
// to full descriptions (cheap proxy — good enough for the warning threshold).
|
|
219
|
+
const anyTruncated = skills.some((skill, i) => {
|
|
220
|
+
const rendered = lines[i] ?? "";
|
|
221
|
+
return skill.description.length > 0 && !rendered.includes(skill.description);
|
|
222
|
+
});
|
|
223
|
+
const warning = buildCatalogWarning(omitted, anyTruncated, budget);
|
|
224
|
+
const out = [
|
|
225
|
+
"## Skills",
|
|
226
|
+
SKILLS_INTRO,
|
|
227
|
+
"### Available skills",
|
|
228
|
+
...lines,
|
|
229
|
+
...(warning ? [warning] : []),
|
|
230
|
+
"### How to use skills",
|
|
231
|
+
SKILLS_HOW_TO_USE,
|
|
232
|
+
];
|
|
233
|
+
return `\n${out.join("\n")}\n`;
|
|
234
|
+
}
|
|
235
|
+
// ─── Layer 2: mention detection (mirrors injection.rs) ────────────────────────
|
|
236
|
+
// injection.rs is_common_env_var — skip `$HOME` etc. so env vars in prose don't
|
|
237
|
+
// masquerade as skill mentions.
|
|
238
|
+
const COMMON_ENV_VARS = new Set([
|
|
239
|
+
"HOME",
|
|
240
|
+
"PATH",
|
|
241
|
+
"PWD",
|
|
242
|
+
"USER",
|
|
243
|
+
"SHELL",
|
|
244
|
+
"LANG",
|
|
245
|
+
"TERM",
|
|
246
|
+
"TMPDIR",
|
|
247
|
+
"EDITOR",
|
|
248
|
+
"HOSTNAME",
|
|
249
|
+
"LOGNAME",
|
|
250
|
+
"OLDPWD",
|
|
251
|
+
]);
|
|
252
|
+
/**
|
|
253
|
+
* Extract `$skill-name` mentions from text (Layer 2).
|
|
254
|
+
* Mirrors injection.rs extract_tool_mentions + select_skills_from_mentions:
|
|
255
|
+
* a plain `$name` only resolves when exactly one skill carries that name.
|
|
256
|
+
* Results preserve `skills` order and are de-duplicated.
|
|
257
|
+
*
|
|
258
|
+
* The codex-rs `[$name](path)` linked form and structured `UserInput::Skill`
|
|
259
|
+
* selection are intentionally not mirrored — ace-mv mentions skills by plain
|
|
260
|
+
* name only.
|
|
261
|
+
*/
|
|
262
|
+
export function extractSkillMentions(text, skills) {
|
|
263
|
+
const mentioned = collectMentionNames(text);
|
|
264
|
+
if (mentioned.size === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
const nameCounts = new Map();
|
|
268
|
+
for (const skill of skills) {
|
|
269
|
+
nameCounts.set(skill.name, (nameCounts.get(skill.name) ?? 0) + 1);
|
|
270
|
+
}
|
|
271
|
+
const selected = [];
|
|
272
|
+
const seen = new Set();
|
|
273
|
+
for (const skill of skills) {
|
|
274
|
+
if (seen.has(skill.name)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (mentioned.has(skill.name) && nameCounts.get(skill.name) === 1) {
|
|
278
|
+
selected.push(skill);
|
|
279
|
+
seen.add(skill.name);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return selected;
|
|
283
|
+
}
|
|
284
|
+
// injection.rs extract_tool_mentions_with_sigil — `$` followed by one or more
|
|
285
|
+
// mention-name chars (ASCII alnum plus '-' '_').
|
|
286
|
+
function collectMentionNames(text) {
|
|
287
|
+
const names = new Set();
|
|
288
|
+
for (const match of text.matchAll(/\$([a-zA-Z0-9_-]+)/g)) {
|
|
289
|
+
const name = match[1];
|
|
290
|
+
if (name && !COMMON_ENV_VARS.has(name)) {
|
|
291
|
+
names.add(name);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return names;
|
|
295
|
+
}
|
|
296
|
+
// ─── Layer 2: full-body envelope (mirrors skill_instructions.rs) ──────────────
|
|
297
|
+
/**
|
|
298
|
+
* Wrap a skill's full SKILL.md body for injection as a turn-scoped user message.
|
|
299
|
+
* Mirrors skill_instructions.rs SkillInstructions::body + the `<skill>` markers.
|
|
300
|
+
*/
|
|
301
|
+
export function renderSkillInjection(skill, contents) {
|
|
302
|
+
return `<skill>\n<name>${skill.name}</name>\n<path>${skill.path}</path>\n${contents}\n</skill>`;
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=skills.js.map
|