@getfamiliar/shared 0.1.2
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/build/AgentRun.d.ts +187 -0
- package/build/AgentRun.js +1 -0
- package/build/AgentRunBus.d.ts +147 -0
- package/build/AgentRunBus.js +362 -0
- package/build/Calendar.d.ts +450 -0
- package/build/Calendar.js +37 -0
- package/build/ChatMessage.d.ts +53 -0
- package/build/ChatMessage.js +11 -0
- package/build/ChatMessageBus.d.ts +110 -0
- package/build/ChatMessageBus.js +257 -0
- package/build/Config.d.ts +83 -0
- package/build/Config.js +1 -0
- package/build/ContainerTool.d.ts +34 -0
- package/build/ContainerTool.js +1 -0
- package/build/CronExpression.d.ts +53 -0
- package/build/CronExpression.js +77 -0
- package/build/Event.d.ts +280 -0
- package/build/Event.js +14 -0
- package/build/EventBus.d.ts +198 -0
- package/build/EventBus.js +482 -0
- package/build/HandlerCatalog.d.ts +75 -0
- package/build/HandlerCatalog.js +165 -0
- package/build/InferenceEvent.d.ts +30 -0
- package/build/InferenceEvent.js +1 -0
- package/build/InferenceEventBus.d.ts +39 -0
- package/build/InferenceEventBus.js +80 -0
- package/build/Mail.d.ts +250 -0
- package/build/Mail.js +73 -0
- package/build/MailStyleTemplate.d.ts +71 -0
- package/build/MailStyleTemplate.js +47 -0
- package/build/ModelMetaData.d.ts +63 -0
- package/build/ModelMetaData.js +12 -0
- package/build/PathGlob.d.ts +20 -0
- package/build/PathGlob.js +33 -0
- package/build/Plugin.d.ts +709 -0
- package/build/Plugin.js +29 -0
- package/build/PostgresConnection.d.ts +75 -0
- package/build/PostgresConnection.js +162 -0
- package/build/ScheduledSubagent.d.ts +49 -0
- package/build/ScheduledSubagent.js +1 -0
- package/build/ScheduledSubagentBus.d.ts +94 -0
- package/build/ScheduledSubagentBus.js +187 -0
- package/build/Schema.d.ts +84 -0
- package/build/Schema.js +570 -0
- package/build/StepResult.d.ts +103 -0
- package/build/StepResult.js +1 -0
- package/build/StepResultBus.d.ts +64 -0
- package/build/StepResultBus.js +169 -0
- package/build/Timezone.d.ts +58 -0
- package/build/Timezone.js +84 -0
- package/build/ToolCall.d.ts +25 -0
- package/build/ToolCall.js +1 -0
- package/build/ToolCallBus.d.ts +65 -0
- package/build/ToolCallBus.js +110 -0
- package/build/ToolLevel.d.ts +20 -0
- package/build/ToolLevel.js +2 -0
- package/build/ToolRunner.d.ts +118 -0
- package/build/ToolRunner.js +192 -0
- package/build/ToolsExpressionUtils.d.ts +80 -0
- package/build/ToolsExpressionUtils.js +102 -0
- package/build/WorkspaceFile.d.ts +104 -0
- package/build/WorkspaceFile.js +7 -0
- package/build/estimateTokens.d.ts +16 -0
- package/build/estimateTokens.js +18 -0
- package/build/index.d.ts +37 -0
- package/build/index.js +26 -0
- package/build/logging/Logger.d.ts +66 -0
- package/build/logging/Logger.js +90 -0
- package/build/markdownHtml.d.ts +14 -0
- package/build/markdownHtml.js +21 -0
- package/build/markdownTerminal.d.ts +14 -0
- package/build/markdownTerminal.js +376 -0
- package/build/testing/MockAgentRunBus.d.ts +52 -0
- package/build/testing/MockAgentRunBus.js +175 -0
- package/build/testing/MockBusStore.d.ts +22 -0
- package/build/testing/MockBusStore.js +22 -0
- package/build/testing/MockEventBus.d.ts +20 -0
- package/build/testing/MockEventBus.js +57 -0
- package/build/testing/MockLogger.d.ts +52 -0
- package/build/testing/MockLogger.js +65 -0
- package/build/testing/index.d.ts +4 -0
- package/build/testing/index.js +4 -0
- package/package.json +63 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-call runner: the shared machinery every agent-facing tool uses
|
|
3
|
+
* to package its result. Three flavors — JSON object, JSON Lines, free
|
|
4
|
+
* text — picked by the tool's `execute` based on what shape it wants
|
|
5
|
+
* the agent to see.
|
|
6
|
+
*
|
|
7
|
+
* Two responsibilities only:
|
|
8
|
+
*
|
|
9
|
+
* - **Truncation / offloading.** When the serialized result's
|
|
10
|
+
* estimated token count (see {@link estimateTokens}) exceeds
|
|
11
|
+
* {@link ToolRunContext.limit}, the runner spills the *full*
|
|
12
|
+
* result to a scratch file via {@link ToolRunContext.spill} and
|
|
13
|
+
* returns either a sentinel ({@link OffloadedJson} for the JSON
|
|
14
|
+
* runner) or a partial-with-marker (JSONL / text runners). The
|
|
15
|
+
* spilled file always carries the complete content so the agent
|
|
16
|
+
* can `fs_read` it for the missing portion.
|
|
17
|
+
* - **Letting throws propagate.** Failure is signalled by throwing.
|
|
18
|
+
* The Vercel AI SDK turns any throw into a `tool-error` block;
|
|
19
|
+
* tools that want a clean message catch and re-throw a
|
|
20
|
+
* {@link ToolError} inside their own body. The runners do not
|
|
21
|
+
* transform errors.
|
|
22
|
+
*
|
|
23
|
+
* Note: success bodies are *not* wrapped in `{ok:true,...}`. The agent
|
|
24
|
+
* receives the bare value (object, JSONL string, or text string).
|
|
25
|
+
*/
|
|
26
|
+
import { Buffer } from "node:buffer";
|
|
27
|
+
/**
|
|
28
|
+
* Default token cap when no config override and no frontmatter override
|
|
29
|
+
* is set. Also doubles as the upper bound in the model-relative offload
|
|
30
|
+
* threshold `min(0.25 * contextLimit, cap)` computed in the agent runner,
|
|
31
|
+
* and as the host gateway's fallback when a caller sends no limit.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DEFAULT_TOOL_CALL_OFFLOADING_LIMIT = 16000;
|
|
34
|
+
/**
|
|
35
|
+
* Convenience throwable for tool bodies that want to attach a stable
|
|
36
|
+
* machine-readable `code` (e.g. `"ErrorItemNotFound"`) and optional
|
|
37
|
+
* HTTP-flavored `status` alongside the human message. The Vercel SDK
|
|
38
|
+
* doesn't surface these fields in `tool-error` blocks, but the gateway
|
|
39
|
+
* transports them across HTTP so transcripts and tests can match on
|
|
40
|
+
* `code` instead of fuzzy-matching on `message`.
|
|
41
|
+
*
|
|
42
|
+
* Plain `Error` throws work too — they propagate verbatim with
|
|
43
|
+
* `code = "ToolError"` synthesized at the wire boundary.
|
|
44
|
+
*/
|
|
45
|
+
export declare class ToolError extends Error {
|
|
46
|
+
readonly code: string;
|
|
47
|
+
readonly status?: number | undefined;
|
|
48
|
+
constructor(code: string, message: string, status?: number | undefined);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Per-call context handed to every runner. Constructed by whichever
|
|
52
|
+
* side (host gateway / container tool wrapper) owns the agentrun.
|
|
53
|
+
*
|
|
54
|
+
* - `limit`: token budget for the inline response (compared against
|
|
55
|
+
* {@link estimateTokens} of the serialized result). Resolved per-call
|
|
56
|
+
* from handler frontmatter → env var / config → default.
|
|
57
|
+
* - `spill`: writes the full result under `/scratch/<event-id>/` and
|
|
58
|
+
* returns the absolute path the agent can later `fs_read`.
|
|
59
|
+
* Implementations dedupe basenames so concurrent calls don't
|
|
60
|
+
* clobber each other.
|
|
61
|
+
*/
|
|
62
|
+
export interface ToolRunContext {
|
|
63
|
+
readonly limit: number;
|
|
64
|
+
spill(suggestedName: string, contents: Buffer): Promise<string>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Sentinel returned by {@link runJsonTool} when the serialized result
|
|
68
|
+
* exceeds the byte budget. The agent should `fs_read` the path for
|
|
69
|
+
* the actual content.
|
|
70
|
+
*/
|
|
71
|
+
export interface OffloadedJson {
|
|
72
|
+
readonly truncated: true;
|
|
73
|
+
readonly fullResultAt: string;
|
|
74
|
+
readonly reason: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Run a tool whose result is a single JSON object. If the
|
|
78
|
+
* `JSON.stringify`'d result's estimated token count is at or below the
|
|
79
|
+
* limit, returns the object verbatim. Otherwise spills the full JSON to
|
|
80
|
+
* scratch and returns {@link OffloadedJson}.
|
|
81
|
+
*
|
|
82
|
+
* No partial JSON is exposed — an oversized result is either fully
|
|
83
|
+
* inline or fully offloaded. The body is expected to throw on failure;
|
|
84
|
+
* the runner does not catch.
|
|
85
|
+
*/
|
|
86
|
+
export declare function runJsonTool(body: () => Promise<object>, ctx: ToolRunContext): Promise<object>;
|
|
87
|
+
/**
|
|
88
|
+
* Run a tool whose result is a stream of JSON objects, serialized as
|
|
89
|
+
* newline-delimited JSON. If the total fits within the limit, returns
|
|
90
|
+
* all lines joined with `\n`. Otherwise spills the full JSONL to
|
|
91
|
+
* scratch and returns the leading whole lines that fit, followed by a
|
|
92
|
+
* marker line:
|
|
93
|
+
*
|
|
94
|
+
* `{"truncated":true,"fullResultAt":"<path>","omittedLines":<N>}`
|
|
95
|
+
*
|
|
96
|
+
* Edge case: when even the first line plus the marker exceeds the
|
|
97
|
+
* limit, only the marker is returned (`omittedLines` then equals the
|
|
98
|
+
* total line count).
|
|
99
|
+
*/
|
|
100
|
+
export declare function runJsonLinesTool(body: () => Promise<Iterable<object> | AsyncIterable<object>>, ctx: ToolRunContext): Promise<string>;
|
|
101
|
+
/**
|
|
102
|
+
* Run a tool whose result is free text. If the text fits the limit,
|
|
103
|
+
* returns it verbatim. Otherwise spills the full text to scratch and
|
|
104
|
+
* returns a truncated prefix followed by a footer pointing at the
|
|
105
|
+
* scratch path:
|
|
106
|
+
*
|
|
107
|
+
* `<truncated text>\n\n[truncated; full result at <path>]`
|
|
108
|
+
*
|
|
109
|
+
* The truncation respects UTF-8 code-point boundaries so multi-byte
|
|
110
|
+
* characters never get split.
|
|
111
|
+
*/
|
|
112
|
+
export declare function runTextTool(body: () => Promise<string>, ctx: ToolRunContext): Promise<string>;
|
|
113
|
+
/**
|
|
114
|
+
* Truncate `str` to at most `maxBytes` UTF-8 bytes without splitting a
|
|
115
|
+
* code point. Surrogate pairs (code points beyond the BMP) are kept
|
|
116
|
+
* together.
|
|
117
|
+
*/
|
|
118
|
+
export declare function truncateUtf8(str: string, maxBytes: number): string;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-call runner: the shared machinery every agent-facing tool uses
|
|
3
|
+
* to package its result. Three flavors — JSON object, JSON Lines, free
|
|
4
|
+
* text — picked by the tool's `execute` based on what shape it wants
|
|
5
|
+
* the agent to see.
|
|
6
|
+
*
|
|
7
|
+
* Two responsibilities only:
|
|
8
|
+
*
|
|
9
|
+
* - **Truncation / offloading.** When the serialized result's
|
|
10
|
+
* estimated token count (see {@link estimateTokens}) exceeds
|
|
11
|
+
* {@link ToolRunContext.limit}, the runner spills the *full*
|
|
12
|
+
* result to a scratch file via {@link ToolRunContext.spill} and
|
|
13
|
+
* returns either a sentinel ({@link OffloadedJson} for the JSON
|
|
14
|
+
* runner) or a partial-with-marker (JSONL / text runners). The
|
|
15
|
+
* spilled file always carries the complete content so the agent
|
|
16
|
+
* can `fs_read` it for the missing portion.
|
|
17
|
+
* - **Letting throws propagate.** Failure is signalled by throwing.
|
|
18
|
+
* The Vercel AI SDK turns any throw into a `tool-error` block;
|
|
19
|
+
* tools that want a clean message catch and re-throw a
|
|
20
|
+
* {@link ToolError} inside their own body. The runners do not
|
|
21
|
+
* transform errors.
|
|
22
|
+
*
|
|
23
|
+
* Note: success bodies are *not* wrapped in `{ok:true,...}`. The agent
|
|
24
|
+
* receives the bare value (object, JSONL string, or text string).
|
|
25
|
+
*/
|
|
26
|
+
import { Buffer } from "node:buffer";
|
|
27
|
+
import { estimateTokens } from "./estimateTokens.js";
|
|
28
|
+
/**
|
|
29
|
+
* Default token cap when no config override and no frontmatter override
|
|
30
|
+
* is set. Also doubles as the upper bound in the model-relative offload
|
|
31
|
+
* threshold `min(0.25 * contextLimit, cap)` computed in the agent runner,
|
|
32
|
+
* and as the host gateway's fallback when a caller sends no limit.
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_TOOL_CALL_OFFLOADING_LIMIT = 16000;
|
|
35
|
+
/**
|
|
36
|
+
* Convenience throwable for tool bodies that want to attach a stable
|
|
37
|
+
* machine-readable `code` (e.g. `"ErrorItemNotFound"`) and optional
|
|
38
|
+
* HTTP-flavored `status` alongside the human message. The Vercel SDK
|
|
39
|
+
* doesn't surface these fields in `tool-error` blocks, but the gateway
|
|
40
|
+
* transports them across HTTP so transcripts and tests can match on
|
|
41
|
+
* `code` instead of fuzzy-matching on `message`.
|
|
42
|
+
*
|
|
43
|
+
* Plain `Error` throws work too — they propagate verbatim with
|
|
44
|
+
* `code = "ToolError"` synthesized at the wire boundary.
|
|
45
|
+
*/
|
|
46
|
+
export class ToolError extends Error {
|
|
47
|
+
code;
|
|
48
|
+
status;
|
|
49
|
+
constructor(code, message, status) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.code = code;
|
|
52
|
+
this.status = status;
|
|
53
|
+
this.name = "ToolError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run a tool whose result is a single JSON object. If the
|
|
58
|
+
* `JSON.stringify`'d result's estimated token count is at or below the
|
|
59
|
+
* limit, returns the object verbatim. Otherwise spills the full JSON to
|
|
60
|
+
* scratch and returns {@link OffloadedJson}.
|
|
61
|
+
*
|
|
62
|
+
* No partial JSON is exposed — an oversized result is either fully
|
|
63
|
+
* inline or fully offloaded. The body is expected to throw on failure;
|
|
64
|
+
* the runner does not catch.
|
|
65
|
+
*/
|
|
66
|
+
export async function runJsonTool(body, ctx) {
|
|
67
|
+
const result = await body();
|
|
68
|
+
const serialized = JSON.stringify(result);
|
|
69
|
+
if (estimateTokens(serialized) <= ctx.limit) {
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
const fullResultAt = await ctx.spill("result.json", Buffer.from(serialized, "utf8"));
|
|
73
|
+
return {
|
|
74
|
+
truncated: true,
|
|
75
|
+
fullResultAt,
|
|
76
|
+
reason: `result exceeded ${ctx.limit} tokens — fs_read the path (it's paginated)`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run a tool whose result is a stream of JSON objects, serialized as
|
|
81
|
+
* newline-delimited JSON. If the total fits within the limit, returns
|
|
82
|
+
* all lines joined with `\n`. Otherwise spills the full JSONL to
|
|
83
|
+
* scratch and returns the leading whole lines that fit, followed by a
|
|
84
|
+
* marker line:
|
|
85
|
+
*
|
|
86
|
+
* `{"truncated":true,"fullResultAt":"<path>","omittedLines":<N>}`
|
|
87
|
+
*
|
|
88
|
+
* Edge case: when even the first line plus the marker exceeds the
|
|
89
|
+
* limit, only the marker is returned (`omittedLines` then equals the
|
|
90
|
+
* total line count).
|
|
91
|
+
*/
|
|
92
|
+
export async function runJsonLinesTool(body, ctx) {
|
|
93
|
+
const source = await body();
|
|
94
|
+
const serialized = [];
|
|
95
|
+
for await (const item of asAsyncIterable(source)) {
|
|
96
|
+
serialized.push(JSON.stringify(item));
|
|
97
|
+
}
|
|
98
|
+
const inline = serialized.join("\n");
|
|
99
|
+
if (estimateTokens(inline) <= ctx.limit) {
|
|
100
|
+
return inline;
|
|
101
|
+
}
|
|
102
|
+
const fullText = inline;
|
|
103
|
+
const fullResultAt = await ctx.spill("result.jsonl", Buffer.from(fullText, "utf8"));
|
|
104
|
+
// Budget the marker assuming worst-case omittedLines (the total
|
|
105
|
+
// line count). Marker can only get shorter as `omittedLines`
|
|
106
|
+
// shrinks, so this is a safe upper bound.
|
|
107
|
+
const upperBoundMarker = JSON.stringify({
|
|
108
|
+
truncated: true,
|
|
109
|
+
fullResultAt,
|
|
110
|
+
omittedLines: serialized.length,
|
|
111
|
+
});
|
|
112
|
+
const markerBudget = estimateTokens(upperBoundMarker);
|
|
113
|
+
const kept = [];
|
|
114
|
+
let usedTokens = 0;
|
|
115
|
+
for (const line of serialized) {
|
|
116
|
+
const separator = kept.length > 0 ? 1 : 0; // newline before this line (~1 token)
|
|
117
|
+
const newlineBeforeMarker = 1; // newline between last kept line and marker
|
|
118
|
+
const addTokens = separator + estimateTokens(line);
|
|
119
|
+
if (usedTokens + addTokens + newlineBeforeMarker + markerBudget > ctx.limit) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
kept.push(line);
|
|
123
|
+
usedTokens += addTokens;
|
|
124
|
+
}
|
|
125
|
+
const omittedLines = serialized.length - kept.length;
|
|
126
|
+
const marker = JSON.stringify({ truncated: true, fullResultAt, omittedLines });
|
|
127
|
+
if (kept.length === 0) {
|
|
128
|
+
return marker;
|
|
129
|
+
}
|
|
130
|
+
return `${kept.join("\n")}\n${marker}`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Run a tool whose result is free text. If the text fits the limit,
|
|
134
|
+
* returns it verbatim. Otherwise spills the full text to scratch and
|
|
135
|
+
* returns a truncated prefix followed by a footer pointing at the
|
|
136
|
+
* scratch path:
|
|
137
|
+
*
|
|
138
|
+
* `<truncated text>\n\n[truncated; full result at <path>]`
|
|
139
|
+
*
|
|
140
|
+
* The truncation respects UTF-8 code-point boundaries so multi-byte
|
|
141
|
+
* characters never get split.
|
|
142
|
+
*/
|
|
143
|
+
export async function runTextTool(body, ctx) {
|
|
144
|
+
const text = await body();
|
|
145
|
+
if (estimateTokens(text) <= ctx.limit) {
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
const fullResultAt = await ctx.spill("result.txt", Buffer.from(text, "utf8"));
|
|
149
|
+
const footer = `\n\n[truncated; full result at ${fullResultAt}]`;
|
|
150
|
+
const footerTokens = estimateTokens(footer);
|
|
151
|
+
// The budget is in tokens; `truncateUtf8` bounds by character count.
|
|
152
|
+
// estimateTokens(s) = ceil(len/4), so the char budget for the prefix
|
|
153
|
+
// is the remaining token budget × 4. Bounding `truncateUtf8` by that
|
|
154
|
+
// char count is slightly conservative for multi-byte text — fine for
|
|
155
|
+
// a placeholder estimator backing a truncation marker.
|
|
156
|
+
const prefixCharBudget = Math.max(0, (ctx.limit - footerTokens) * 4);
|
|
157
|
+
const prefix = truncateUtf8(text, prefixCharBudget);
|
|
158
|
+
return `${prefix}${footer}`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Truncate `str` to at most `maxBytes` UTF-8 bytes without splitting a
|
|
162
|
+
* code point. Surrogate pairs (code points beyond the BMP) are kept
|
|
163
|
+
* together.
|
|
164
|
+
*/
|
|
165
|
+
export function truncateUtf8(str, maxBytes) {
|
|
166
|
+
if (maxBytes <= 0) {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
let bytes = 0;
|
|
170
|
+
let charIdx = 0;
|
|
171
|
+
while (charIdx < str.length) {
|
|
172
|
+
const cp = str.codePointAt(charIdx);
|
|
173
|
+
if (cp === undefined) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
const cpBytes = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
|
|
177
|
+
if (bytes + cpBytes > maxBytes) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
bytes += cpBytes;
|
|
181
|
+
charIdx += cp >= 0x10000 ? 2 : 1;
|
|
182
|
+
}
|
|
183
|
+
return str.slice(0, charIdx);
|
|
184
|
+
}
|
|
185
|
+
/** Lift a sync iterable into an async one so we can `for await` over both. */
|
|
186
|
+
async function* asAsyncIterable(source) {
|
|
187
|
+
if (Symbol.asyncIterator in source) {
|
|
188
|
+
yield* source;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
yield* source;
|
|
192
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-cutting constants for tool-group naming. A handler's `tools:`
|
|
3
|
+
* frontmatter lists explicit tool names, `*`-globs, or group names;
|
|
4
|
+
* the resolver lives in the container
|
|
5
|
+
* (`container/src/tools/ToolsExpressionParser.ts`). The pieces
|
|
6
|
+
* re-exported here are the ones the host-side `mcp.yml` linter and the
|
|
7
|
+
* plugin tool registry also need to enforce — namely the reserved
|
|
8
|
+
* group names and the shape every declarable name must match.
|
|
9
|
+
*
|
|
10
|
+
* Group names are otherwise *open*: any identifier matching
|
|
11
|
+
* {@link IDENT_PATTERN} is a valid group, and a tool joins it by
|
|
12
|
+
* listing the name in `PluginTool.groups`. The "predefined" groups
|
|
13
|
+
* (`core`, `fs`, `reflection`, …) are not hardcoded sets — they
|
|
14
|
+
* emerge from the union of every tool that declares them.
|
|
15
|
+
*/
|
|
16
|
+
/** Built-in group: every key in the available pool (system ∪ MCP). Reserved. */
|
|
17
|
+
export declare const ALL_GROUP_NAME = "all";
|
|
18
|
+
/** Built-in group: every MCP-tool key. Reserved. */
|
|
19
|
+
export declare const MCP_GROUP_NAME = "mcp";
|
|
20
|
+
/** Built-in group: empty set — used to override a parent's `tools:`. Reserved. */
|
|
21
|
+
export declare const NONE_GROUP_NAME = "none";
|
|
22
|
+
/**
|
|
23
|
+
* Reserved plugin id for host-owned tools registered without a
|
|
24
|
+
* plugin-id prefix (the "bare-key" path — `cal_get_events` rather
|
|
25
|
+
* than `core_cal_get_events`). It is a **registration sentinel, not
|
|
26
|
+
* an addressable tool group**: it deliberately collides in spelling
|
|
27
|
+
* with the curated `core` group, so it must never be turned into a
|
|
28
|
+
* plugin-id auto-group (doing so would shadow the curated `core`
|
|
29
|
+
* group — the union of container built-ins and tools declaring
|
|
30
|
+
* `groups: ["core"]` — with the full set of host-owned tools). Host
|
|
31
|
+
* built-in tools join curated groups through their own `groups`
|
|
32
|
+
* field (`["cal"]`, `["mail"]`, `["reflection"]`, …) instead.
|
|
33
|
+
*/
|
|
34
|
+
export declare const CORE_PLUGIN_ID = "core";
|
|
35
|
+
/**
|
|
36
|
+
* Names the resolver handles directly and that therefore cannot
|
|
37
|
+
* appear as a declarable group, an MCP id, or a plugin id. Using one
|
|
38
|
+
* as an MCP id in `mcp.yml` is rejected by the loader; declaring it in
|
|
39
|
+
* `PluginTool.groups` is rejected by the plugin tool registry.
|
|
40
|
+
*/
|
|
41
|
+
export declare const RESERVED_GROUP_NAMES: ReadonlySet<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Pattern any group name — declarable, MCP-id-derived, or
|
|
44
|
+
* plugin-id-derived — must match.
|
|
45
|
+
*
|
|
46
|
+
* Lowercase alphanumeric only, leading letter, no underscores and
|
|
47
|
+
* no hyphens. The exclusion of `_` is load-bearing: tool keys are
|
|
48
|
+
* always shaped `${id}_${name}` (plugin tools) or a bare alnum-only
|
|
49
|
+
* stem (built-ins) and contain at least one underscore in the
|
|
50
|
+
* namespaced form, so an underscore-free bareword can never collide
|
|
51
|
+
* with a tool key — the tool-group vs tool-pattern classifier in
|
|
52
|
+
* `resolveTools` uses that to tell them apart structurally.
|
|
53
|
+
*/
|
|
54
|
+
export declare const IDENT_PATTERN: RegExp;
|
|
55
|
+
/**
|
|
56
|
+
* Validate a declared group name (used by the host plugin tool
|
|
57
|
+
* registry and the container's built-in tool registration). Throws
|
|
58
|
+
* with a self-contained message if the name is reserved or fails
|
|
59
|
+
* {@link IDENT_PATTERN}.
|
|
60
|
+
*/
|
|
61
|
+
export declare function validateGroupName(name: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Match one tool-name glob pattern against a single tool name. A
|
|
64
|
+
* pattern without `*` is an exact match; `*` is a wildcard for any
|
|
65
|
+
* character sequence (including `_`, since tool names contain `_`).
|
|
66
|
+
* Fully anchored (`^…$`) — `comment_*` matches `comment_reply` but not
|
|
67
|
+
* `commentary`. This is the shared semantics used by the container's
|
|
68
|
+
* `tools:` frontmatter resolver and by the `mcp.yml` allow/deny/level
|
|
69
|
+
* globs.
|
|
70
|
+
*
|
|
71
|
+
* @param pattern The glob pattern (bareword or `*`-containing).
|
|
72
|
+
* @param name The tool name to test.
|
|
73
|
+
* @returns `true` when the name matches the pattern.
|
|
74
|
+
*/
|
|
75
|
+
export declare function toolPatternMatches(pattern: string, name: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* `true` when `name` matches any of the glob `patterns`. Empty list ⇒
|
|
78
|
+
* `false`. See {@link toolPatternMatches} for the per-pattern semantics.
|
|
79
|
+
*/
|
|
80
|
+
export declare function matchesAnyToolPattern(patterns: readonly string[], name: string): boolean;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-cutting constants for tool-group naming. A handler's `tools:`
|
|
3
|
+
* frontmatter lists explicit tool names, `*`-globs, or group names;
|
|
4
|
+
* the resolver lives in the container
|
|
5
|
+
* (`container/src/tools/ToolsExpressionParser.ts`). The pieces
|
|
6
|
+
* re-exported here are the ones the host-side `mcp.yml` linter and the
|
|
7
|
+
* plugin tool registry also need to enforce — namely the reserved
|
|
8
|
+
* group names and the shape every declarable name must match.
|
|
9
|
+
*
|
|
10
|
+
* Group names are otherwise *open*: any identifier matching
|
|
11
|
+
* {@link IDENT_PATTERN} is a valid group, and a tool joins it by
|
|
12
|
+
* listing the name in `PluginTool.groups`. The "predefined" groups
|
|
13
|
+
* (`core`, `fs`, `reflection`, …) are not hardcoded sets — they
|
|
14
|
+
* emerge from the union of every tool that declares them.
|
|
15
|
+
*/
|
|
16
|
+
/** Built-in group: every key in the available pool (system ∪ MCP). Reserved. */
|
|
17
|
+
export const ALL_GROUP_NAME = "all";
|
|
18
|
+
/** Built-in group: every MCP-tool key. Reserved. */
|
|
19
|
+
export const MCP_GROUP_NAME = "mcp";
|
|
20
|
+
/** Built-in group: empty set — used to override a parent's `tools:`. Reserved. */
|
|
21
|
+
export const NONE_GROUP_NAME = "none";
|
|
22
|
+
/**
|
|
23
|
+
* Reserved plugin id for host-owned tools registered without a
|
|
24
|
+
* plugin-id prefix (the "bare-key" path — `cal_get_events` rather
|
|
25
|
+
* than `core_cal_get_events`). It is a **registration sentinel, not
|
|
26
|
+
* an addressable tool group**: it deliberately collides in spelling
|
|
27
|
+
* with the curated `core` group, so it must never be turned into a
|
|
28
|
+
* plugin-id auto-group (doing so would shadow the curated `core`
|
|
29
|
+
* group — the union of container built-ins and tools declaring
|
|
30
|
+
* `groups: ["core"]` — with the full set of host-owned tools). Host
|
|
31
|
+
* built-in tools join curated groups through their own `groups`
|
|
32
|
+
* field (`["cal"]`, `["mail"]`, `["reflection"]`, …) instead.
|
|
33
|
+
*/
|
|
34
|
+
export const CORE_PLUGIN_ID = "core";
|
|
35
|
+
/**
|
|
36
|
+
* Names the resolver handles directly and that therefore cannot
|
|
37
|
+
* appear as a declarable group, an MCP id, or a plugin id. Using one
|
|
38
|
+
* as an MCP id in `mcp.yml` is rejected by the loader; declaring it in
|
|
39
|
+
* `PluginTool.groups` is rejected by the plugin tool registry.
|
|
40
|
+
*/
|
|
41
|
+
export const RESERVED_GROUP_NAMES = new Set([
|
|
42
|
+
ALL_GROUP_NAME,
|
|
43
|
+
MCP_GROUP_NAME,
|
|
44
|
+
NONE_GROUP_NAME,
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* Pattern any group name — declarable, MCP-id-derived, or
|
|
48
|
+
* plugin-id-derived — must match.
|
|
49
|
+
*
|
|
50
|
+
* Lowercase alphanumeric only, leading letter, no underscores and
|
|
51
|
+
* no hyphens. The exclusion of `_` is load-bearing: tool keys are
|
|
52
|
+
* always shaped `${id}_${name}` (plugin tools) or a bare alnum-only
|
|
53
|
+
* stem (built-ins) and contain at least one underscore in the
|
|
54
|
+
* namespaced form, so an underscore-free bareword can never collide
|
|
55
|
+
* with a tool key — the tool-group vs tool-pattern classifier in
|
|
56
|
+
* `resolveTools` uses that to tell them apart structurally.
|
|
57
|
+
*/
|
|
58
|
+
export const IDENT_PATTERN = /^[a-z][a-z0-9]*$/;
|
|
59
|
+
/**
|
|
60
|
+
* Validate a declared group name (used by the host plugin tool
|
|
61
|
+
* registry and the container's built-in tool registration). Throws
|
|
62
|
+
* with a self-contained message if the name is reserved or fails
|
|
63
|
+
* {@link IDENT_PATTERN}.
|
|
64
|
+
*/
|
|
65
|
+
export function validateGroupName(name) {
|
|
66
|
+
if (!IDENT_PATTERN.test(name)) {
|
|
67
|
+
throw new Error(`tool group name "${name}" is not a valid tool group name (must match ${IDENT_PATTERN})`);
|
|
68
|
+
}
|
|
69
|
+
if (RESERVED_GROUP_NAMES.has(name)) {
|
|
70
|
+
throw new Error(`tool group name "${name}" is a reserved tool group name (${[...RESERVED_GROUP_NAMES].join(", ")})`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Match one tool-name glob pattern against a single tool name. A
|
|
75
|
+
* pattern without `*` is an exact match; `*` is a wildcard for any
|
|
76
|
+
* character sequence (including `_`, since tool names contain `_`).
|
|
77
|
+
* Fully anchored (`^…$`) — `comment_*` matches `comment_reply` but not
|
|
78
|
+
* `commentary`. This is the shared semantics used by the container's
|
|
79
|
+
* `tools:` frontmatter resolver and by the `mcp.yml` allow/deny/level
|
|
80
|
+
* globs.
|
|
81
|
+
*
|
|
82
|
+
* @param pattern The glob pattern (bareword or `*`-containing).
|
|
83
|
+
* @param name The tool name to test.
|
|
84
|
+
* @returns `true` when the name matches the pattern.
|
|
85
|
+
*/
|
|
86
|
+
export function toolPatternMatches(pattern, name) {
|
|
87
|
+
if (!pattern.includes("*")) {
|
|
88
|
+
return pattern === name;
|
|
89
|
+
}
|
|
90
|
+
const regex = new RegExp(`^${pattern
|
|
91
|
+
.split("*")
|
|
92
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
93
|
+
.join(".*")}$`);
|
|
94
|
+
return regex.test(name);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `true` when `name` matches any of the glob `patterns`. Empty list ⇒
|
|
98
|
+
* `false`. See {@link toolPatternMatches} for the per-pattern semantics.
|
|
99
|
+
*/
|
|
100
|
+
export function matchesAnyToolPattern(patterns, name) {
|
|
101
|
+
return patterns.some((pattern) => toolPatternMatches(pattern, name));
|
|
102
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types describing the workspace-file surface plugins observe via
|
|
3
|
+
* `ctx.workspace.*`. The concrete watcher lives in the host
|
|
4
|
+
* (`host/src/workspace/WorkspaceWatcher.ts`); host and plugins share the
|
|
5
|
+
* same `WorkspaceFile` shape — no projection layer.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Filter describing which workspace files a subscriber cares about.
|
|
9
|
+
*
|
|
10
|
+
* All fields are optional and combined with AND semantics. An empty
|
|
11
|
+
* filter matches every file in scope (the watcher's scope is markdown
|
|
12
|
+
* only — see {@link WorkspaceWatcherApi}).
|
|
13
|
+
*/
|
|
14
|
+
export interface WorkspaceFileFilter {
|
|
15
|
+
/**
|
|
16
|
+
* Frontmatter key → search expression. The file matches iff every
|
|
17
|
+
* key listed here is present in the file's YAML frontmatter AND
|
|
18
|
+
* the stringified value matches the expression.
|
|
19
|
+
*
|
|
20
|
+
* The expression is a substring matcher with `*` as the any-chars
|
|
21
|
+
* wildcard. `"*"` alone means "key present with any value". `"foo"`
|
|
22
|
+
* means "value contains the substring `foo`". `"foo*bar"` means
|
|
23
|
+
* "value contains `foo` somewhere, then `bar` later".
|
|
24
|
+
*/
|
|
25
|
+
readonly frontmatter?: Readonly<Record<string, string>>;
|
|
26
|
+
/**
|
|
27
|
+
* Optional substring-with-`*` glob applied to the workspace-relative
|
|
28
|
+
* path. Same wildcard grammar as the frontmatter values.
|
|
29
|
+
*/
|
|
30
|
+
readonly pathGlob?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A workspace file observed by the watcher.
|
|
34
|
+
*
|
|
35
|
+
* Returned by {@link WorkspaceWatcherApi.listMarkdownFiles} (with `kind`
|
|
36
|
+
* omitted) and dispatched to {@link WorkspaceWatcherApi.onMarkdownFileUpdate}
|
|
37
|
+
* callbacks (with `kind` set to the transition that triggered the
|
|
38
|
+
* notification).
|
|
39
|
+
*
|
|
40
|
+
* `removed` may be synthetic: a file whose frontmatter changed such
|
|
41
|
+
* that it no longer matches a subscription's filter is reported as
|
|
42
|
+
* `removed` even though the file still exists on disk. The file is
|
|
43
|
+
* gone *from the subscriber's point of view*, which is the only thing
|
|
44
|
+
* the consumer can act on.
|
|
45
|
+
*/
|
|
46
|
+
export interface WorkspaceFile {
|
|
47
|
+
/** Path relative to the workspace root, POSIX separators. */
|
|
48
|
+
readonly relativePath: string;
|
|
49
|
+
/** Absolute path on disk. */
|
|
50
|
+
readonly absolutePath: string;
|
|
51
|
+
/**
|
|
52
|
+
* Transition kind. Always set on `onMarkdownFileUpdate` callbacks
|
|
53
|
+
* (`"added"` on first match — creation or a frontmatter edit that
|
|
54
|
+
* brings the file into scope; `"changed"` for content edits while
|
|
55
|
+
* still in scope; `"removed"` on the reverse transition). Always
|
|
56
|
+
* omitted on `listMarkdownFiles` results.
|
|
57
|
+
*/
|
|
58
|
+
readonly kind?: "added" | "changed" | "removed";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The `ctx.workspace` surface plugins use to observe workspace files.
|
|
62
|
+
*
|
|
63
|
+
* **Scope: markdown only.** Only `.md` files under the workspace root
|
|
64
|
+
* are tracked. Both methods below ignore every other extension; an
|
|
65
|
+
* empty filter still only matches `.md` files. (The method names
|
|
66
|
+
* spell this out — `listMarkdownFiles`, `onMarkdownFileUpdate` — so
|
|
67
|
+
* the scope is impossible to miss at the call site.)
|
|
68
|
+
*
|
|
69
|
+
* The intended pattern is snapshot-then-diff: call
|
|
70
|
+
* {@link listMarkdownFiles} once for the baseline, then
|
|
71
|
+
* {@link onMarkdownFileUpdate} for live transitions. Both calls take
|
|
72
|
+
* the same {@link WorkspaceFileFilter}; together they give a plugin a
|
|
73
|
+
* complete view (baseline + live transitions) without the watcher
|
|
74
|
+
* having to re-announce pre-existing files on subscription.
|
|
75
|
+
*
|
|
76
|
+
* Example:
|
|
77
|
+
*
|
|
78
|
+
* const snapshot = await ctx.workspace.listMarkdownFiles(filter);
|
|
79
|
+
* for (const file of snapshot) { ...seed... }
|
|
80
|
+
* const unsub = ctx.workspace.onMarkdownFileUpdate(filter, handleDelta);
|
|
81
|
+
*
|
|
82
|
+
* Only available inside the daemon — CLI subcommands run without a
|
|
83
|
+
* live watcher, so both methods throw with an explicit message in
|
|
84
|
+
* that mode.
|
|
85
|
+
*/
|
|
86
|
+
export interface WorkspaceWatcherApi {
|
|
87
|
+
/**
|
|
88
|
+
* Snapshot of every workspace `.md` file currently matching the
|
|
89
|
+
* filter. Reads from the watcher's in-memory cache (no fresh disk
|
|
90
|
+
* scan); chokidar keeps that cache continuously in sync with disk.
|
|
91
|
+
* Returned entries have `kind` omitted. Pair with
|
|
92
|
+
* {@link onMarkdownFileUpdate} — see the interface-level doc.
|
|
93
|
+
*/
|
|
94
|
+
listMarkdownFiles(filter: WorkspaceFileFilter): Promise<readonly WorkspaceFile[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Subscribe to `.md` file transitions matching the filter. The
|
|
97
|
+
* callback fires per transition with `kind` always set. Pre-existing
|
|
98
|
+
* matches at subscription time are **not** replayed — call
|
|
99
|
+
* {@link listMarkdownFiles} for the baseline.
|
|
100
|
+
*
|
|
101
|
+
* Returns the unsubscribe handle.
|
|
102
|
+
*/
|
|
103
|
+
onMarkdownFileUpdate(filter: WorkspaceFileFilter, callback: (file: WorkspaceFile) => void): () => void;
|
|
104
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types describing the workspace-file surface plugins observe via
|
|
3
|
+
* `ctx.workspace.*`. The concrete watcher lives in the host
|
|
4
|
+
* (`host/src/workspace/WorkspaceWatcher.ts`); host and plugins share the
|
|
5
|
+
* same `WorkspaceFile` shape — no projection layer.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estimate the number of tokens a string occupies in a model's context
|
|
3
|
+
* window. Used to bound tool-result offloading (see {@link import("./ToolRunner.js")})
|
|
4
|
+
* and active context-window management in the container's agent loop.
|
|
5
|
+
*
|
|
6
|
+
* The current heuristic is a coarse `~4 characters per token` — fast,
|
|
7
|
+
* dependency-free, and good enough for the budgeting decisions it backs
|
|
8
|
+
* (offload-or-not, drop-or-not). It deliberately over- nor under-counts
|
|
9
|
+
* by much for typical English/JSON payloads. A real tokenizer
|
|
10
|
+
* (e.g. tiktoken) can be slotted in here later without touching callers.
|
|
11
|
+
*
|
|
12
|
+
* @param text The string to estimate.
|
|
13
|
+
* @returns The estimated token count (always a non-negative integer;
|
|
14
|
+
* `0` for the empty string).
|
|
15
|
+
*/
|
|
16
|
+
export declare function estimateTokens(text: string): number;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estimate the number of tokens a string occupies in a model's context
|
|
3
|
+
* window. Used to bound tool-result offloading (see {@link import("./ToolRunner.js")})
|
|
4
|
+
* and active context-window management in the container's agent loop.
|
|
5
|
+
*
|
|
6
|
+
* The current heuristic is a coarse `~4 characters per token` — fast,
|
|
7
|
+
* dependency-free, and good enough for the budgeting decisions it backs
|
|
8
|
+
* (offload-or-not, drop-or-not). It deliberately over- nor under-counts
|
|
9
|
+
* by much for typical English/JSON payloads. A real tokenizer
|
|
10
|
+
* (e.g. tiktoken) can be slotted in here later without touching callers.
|
|
11
|
+
*
|
|
12
|
+
* @param text The string to estimate.
|
|
13
|
+
* @returns The estimated token count (always a non-negative integer;
|
|
14
|
+
* `0` for the empty string).
|
|
15
|
+
*/
|
|
16
|
+
export function estimateTokens(text) {
|
|
17
|
+
return Math.ceil(text.length / 4);
|
|
18
|
+
}
|