agent-accelerator 0.4.0 → 0.5.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/README.md +149 -57
- package/bunfig.toml +1 -0
- package/package.json +2 -3
- package/src/agent/agent.ts +414 -118
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +327 -99
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +58 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +47 -2
- package/src/providers/google.ts +338 -81
- package/src/providers/openai-compat.ts +171 -61
- package/src/providers/openai.ts +174 -44
- package/src/providers/openrouter.ts +129 -44
- package/src/providers/shared.ts +169 -17
- package/src/providers.ts +375 -11
- package/src/scheduler/scheduled-actions.md +41 -0
- package/src/scheduler/scheduler.ts +814 -0
- package/src/scheduler/store.ts +263 -0
- package/src/scheduler/time.ts +148 -0
- package/src/scheduler/tool.ts +151 -0
- package/src/session/store.ts +229 -113
- package/src/streaming/event-stream.ts +6 -4
- package/src/telemetry-file.ts +31 -0
- package/src/telemetry.ts +65 -0
- package/src/tools/executor.ts +163 -38
- package/src/tools/subagent-result.ts +60 -0
- package/src/types/agent.ts +74 -22
- package/src/types/model.ts +39 -0
- package/src/types/response.ts +9 -0
- package/src/types/scheduler.ts +81 -0
- package/src/types/tool.ts +15 -0
- package/src/utils/cache.ts +14 -5
- package/src/utils/documents.ts +112 -52
- package/src/utils/env.ts +21 -7
- package/src/utils/errors.ts +26 -1
- package/src/utils/headers.ts +15 -15
- package/src/utils/home.ts +30 -0
- package/src/utils/media.ts +57 -12
- package/src/utils/raw-capture.ts +76 -0
- package/src/utils/retry.ts +11 -3
- package/src/utils/safe-io.ts +252 -0
- package/src/utils/session.ts +7 -8
- package/src/data/README.md +0 -84
package/src/types/response.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SubAgentStep {
|
|
|
9
9
|
name?: string;
|
|
10
10
|
text?: string;
|
|
11
11
|
thinking?: string;
|
|
12
|
+
/** Opaque provider thought signature captured with this step's reasoning, when present. */
|
|
13
|
+
thoughtSignature?: string;
|
|
12
14
|
isError?: boolean;
|
|
13
15
|
timestamp: number;
|
|
14
16
|
/** True while the worker is still streaming this step; finalized on turn end. */
|
|
@@ -101,6 +103,8 @@ export function normalizeFinishReason(raw?: string): {
|
|
|
101
103
|
/** JSON representation returned by AgentResponse.toJSON(). */
|
|
102
104
|
export interface AgentResponseJSON {
|
|
103
105
|
text: string;
|
|
106
|
+
/** Validated structured-output value when `AgentConfig.output` was set. */
|
|
107
|
+
parsed?: unknown;
|
|
104
108
|
thinking?: string;
|
|
105
109
|
thoughtSignature?: string;
|
|
106
110
|
toolCalls?: ToolCallRecord[];
|
|
@@ -121,6 +125,8 @@ export interface AgentResponseJSON {
|
|
|
121
125
|
/** Normalized final result returned by every Agent run. */
|
|
122
126
|
export class AgentResponse {
|
|
123
127
|
readonly text: string;
|
|
128
|
+
/** Validated structured-output value when `AgentConfig.output` was set. */
|
|
129
|
+
readonly parsed?: unknown;
|
|
124
130
|
readonly thinking?: string;
|
|
125
131
|
readonly thoughtSignature?: string;
|
|
126
132
|
readonly toolCalls: ToolCallRecord[];
|
|
@@ -146,6 +152,7 @@ export class AgentResponse {
|
|
|
146
152
|
*/
|
|
147
153
|
constructor(data: {
|
|
148
154
|
text: string;
|
|
155
|
+
parsed?: unknown;
|
|
149
156
|
thinking?: string;
|
|
150
157
|
thoughtSignature?: string;
|
|
151
158
|
toolCalls?: ToolCallRecord[];
|
|
@@ -162,6 +169,7 @@ export class AgentResponse {
|
|
|
162
169
|
turns?: number;
|
|
163
170
|
}) {
|
|
164
171
|
this.text = data.text;
|
|
172
|
+
if (data.parsed !== undefined) (this as { parsed?: unknown }).parsed = data.parsed;
|
|
165
173
|
this.thinking = data.thinking;
|
|
166
174
|
this.thoughtSignature = data.thoughtSignature;
|
|
167
175
|
this.toolCalls = data.toolCalls ?? [];
|
|
@@ -187,6 +195,7 @@ export class AgentResponse {
|
|
|
187
195
|
toJSON(): AgentResponseJSON {
|
|
188
196
|
return {
|
|
189
197
|
text: this.text,
|
|
198
|
+
...(this.parsed !== undefined ? { parsed: this.parsed } : {}),
|
|
190
199
|
thinking: this.thinking,
|
|
191
200
|
thoughtSignature: this.thoughtSignature,
|
|
192
201
|
toolCalls: this.toolCalls.length > 0 ? this.toolCalls : undefined,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Lifecycle of a scheduled action as stored in `tasks.json`. */
|
|
2
|
+
export type ScheduledState = "waiting" | "cooking" | "missed" | "done";
|
|
3
|
+
|
|
4
|
+
/** Outcome of one run. */
|
|
5
|
+
export type ScheduledRunStatus = "ok" | "failed";
|
|
6
|
+
|
|
7
|
+
/** Schedule fields shared by `create` input and the stored record. */
|
|
8
|
+
export interface ScheduleSpec {
|
|
9
|
+
/** Delivery clock time `DD:HH:MM`: `DD` days from today, `HH:MM` on a 12-hour clock (see `meridiem`). */
|
|
10
|
+
timeToDeliver: string;
|
|
11
|
+
meridiem: "AM" | "PM";
|
|
12
|
+
/** Duration `DD:HH:MM` before delivery at which the run starts. Default 10 minutes (`00:00:10`). */
|
|
13
|
+
startCookingInAdvance?: string;
|
|
14
|
+
/** Duration `DD:HH:MM` between deliveries (`01:00:00` daily). Omit to run once. */
|
|
15
|
+
repeatEvery?: string;
|
|
16
|
+
/** Extra attempts if a run fails, only while time remains before delivery. Default 0. */
|
|
17
|
+
retries?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What `createScheduledAction` receives: dynamic sub-agent fields plus the schedule. */
|
|
21
|
+
export interface ScheduledActionInput {
|
|
22
|
+
role?: string;
|
|
23
|
+
instructions: string;
|
|
24
|
+
task: string;
|
|
25
|
+
/** Names from the developer's `dynamicSubagents.tools` pool. */
|
|
26
|
+
tools?: string[];
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
scheduled_action: ScheduleSpec & {
|
|
29
|
+
/** Human label. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Lowercase kebab-case, max 48 chars. The SDK appends 6 random chars. */
|
|
32
|
+
id: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What `updateScheduledAction` may change. `id` and the creating agent are immutable. */
|
|
37
|
+
export type ScheduledActionPatch = Partial<Omit<ScheduledActionInput, "scheduled_action">> & {
|
|
38
|
+
scheduled_action?: Partial<Omit<ScheduledActionInput["scheduled_action"], "id">>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** One entry of `tasks.json`. All times are local clock strings `YYYY-MM-DDTHH:mm:ss` (no offset). */
|
|
42
|
+
export interface ScheduledActionRecord {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
/** Name of the creating Agent; it runs the worker. */
|
|
46
|
+
agent: string;
|
|
47
|
+
role?: string;
|
|
48
|
+
instructions: string;
|
|
49
|
+
task: string;
|
|
50
|
+
/** Tool NAMES only; resolved against the live agent's pool at run time. */
|
|
51
|
+
tools: string[];
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
/** The schedule as given. The stored truth is `nextCookAt` / `nextDeliverAt`. */
|
|
54
|
+
schedule: ScheduleSpec & { startCookingInAdvance: string };
|
|
55
|
+
state: ScheduledState;
|
|
56
|
+
/** `null` for a finished one-shot. */
|
|
57
|
+
nextCookAt: string | null;
|
|
58
|
+
nextDeliverAt: string | null;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
/** Present only while `state` is `cooking`. */
|
|
61
|
+
current?: { cookStartedAt: string; deliverAt: string };
|
|
62
|
+
lastRun?: { status: ScheduledRunStatus; file: string; deliverAt: string; deliveredAt: string; late: boolean };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Payload of the `scheduled_action.delivered` telemetry event. Plain JSON. */
|
|
66
|
+
export interface ScheduledDelivery {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
status: ScheduledRunStatus;
|
|
70
|
+
/** Absolute path of the run file. */
|
|
71
|
+
file: string;
|
|
72
|
+
readyAt: string;
|
|
73
|
+
deliverAt: string;
|
|
74
|
+
late: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Result of `killScheduledAction`. */
|
|
78
|
+
export type KillResult =
|
|
79
|
+
| { ok: true }
|
|
80
|
+
| { ok: false; reason: "not_found" }
|
|
81
|
+
| { ok: false; reason: "running"; message: string };
|
package/src/types/tool.ts
CHANGED
|
@@ -136,3 +136,18 @@ export interface ToolResultRecord {
|
|
|
136
136
|
isError?: boolean;
|
|
137
137
|
durationMs?: number;
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Key of the marker object that replaces tool arguments whose streamed JSON
|
|
142
|
+
* never parsed (`{ __truncated_raw: "<partial text>" }`). It is deliberately
|
|
143
|
+
* not a plausible parameter name, so a real tool with a `raw` parameter is
|
|
144
|
+
* never mistaken for a truncated call. Such calls are never executed.
|
|
145
|
+
*/
|
|
146
|
+
export const TRUNCATED_ARGS_KEY = "__truncated_raw";
|
|
147
|
+
|
|
148
|
+
/** True when `args` is exactly the truncated-arguments marker. */
|
|
149
|
+
export function isTruncatedArguments(args: unknown): boolean {
|
|
150
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return false;
|
|
151
|
+
const keys = Object.keys(args as Record<string, unknown>);
|
|
152
|
+
return keys.length === 1 && keys[0] === TRUNCATED_ARGS_KEY && typeof (args as Record<string, unknown>)[TRUNCATED_ARGS_KEY] === "string";
|
|
153
|
+
}
|
package/src/utils/cache.ts
CHANGED
|
@@ -71,7 +71,7 @@ export interface CreateExplicitCacheOptions {
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Creates an explicit cached content object using Google Gemini's cachedContents API.
|
|
74
|
-
* REST: POST https://generativelanguage.googleapis.com/v1beta/cachedContents
|
|
74
|
+
* REST: POST https://generativelanguage.googleapis.com/v1beta/cachedContents (key in the `x-goog-api-key` header)
|
|
75
75
|
*/
|
|
76
76
|
/**
|
|
77
77
|
* Creates a Google Gemini cachedContents resource through the REST API.
|
|
@@ -125,11 +125,13 @@ export async function createExplicitCache(
|
|
|
125
125
|
(payload as any).tool_config = options.toolConfig;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
// The key travels in a header, never in the URL (URLs end up in proxy and access logs).
|
|
129
|
+
const url = `${baseUrl}/cachedContents`;
|
|
129
130
|
const response = await fetch(url, {
|
|
130
131
|
method: "POST",
|
|
131
132
|
headers: {
|
|
132
133
|
"Content-Type": "application/json",
|
|
134
|
+
"x-goog-api-key": apiKey,
|
|
133
135
|
},
|
|
134
136
|
body: JSON.stringify(payload),
|
|
135
137
|
});
|
|
@@ -152,8 +154,15 @@ export async function createExplicitCache(
|
|
|
152
154
|
/** Header-name deny-list (case-insensitive substring match). */
|
|
153
155
|
const REDACT_HEADER_SUBSTRINGS = ["key", "token", "secret", "cookie", "authorization"];
|
|
154
156
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Credential-looking values. Anchored on a non-alphanumeric boundary so plain
|
|
159
|
+
* words that merely contain "sk-" (e.g. `Task-Manager`, `Disk-Cache`) are NOT
|
|
160
|
+
* redacted, while `sk-…`, `gsk_…`, `xoxb-…` and `AIza…` tokens are.
|
|
161
|
+
*/
|
|
162
|
+
const SENSITIVE_VALUE_PATTERNS: RegExp[] = [
|
|
163
|
+
/(^|[^A-Za-z0-9])(sk|gsk|xox[a-z]?)[-_][A-Za-z0-9_-]{8,}/,
|
|
164
|
+
/(^|[^A-Za-z0-9])AIza[A-Za-z0-9_-]{10,}/,
|
|
165
|
+
];
|
|
157
166
|
|
|
158
167
|
function headerNameIsSensitive(name: string): boolean {
|
|
159
168
|
const lower = String(name ?? "").toLowerCase();
|
|
@@ -163,7 +172,7 @@ function headerNameIsSensitive(name: string): boolean {
|
|
|
163
172
|
function valueLooksSensitive(value: unknown): boolean {
|
|
164
173
|
if (typeof value !== "string") return false;
|
|
165
174
|
const v = value.trim();
|
|
166
|
-
if (
|
|
175
|
+
if (SENSITIVE_VALUE_PATTERNS.some((re) => re.test(v))) return true;
|
|
167
176
|
// Bearer-style long tokens: "Bearer <20+ chars>"
|
|
168
177
|
if (/^bearer\s+\S{20,}/i.test(v)) return true;
|
|
169
178
|
return false;
|
package/src/utils/documents.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { normalizeMediaInput } from "./media.ts";
|
|
|
6
6
|
import { base64ToBytes } from "./base64.ts";
|
|
7
7
|
import { escapeXml } from "./serialization.ts";
|
|
8
8
|
import { getModelFromCatalog } from "../models/catalog.ts";
|
|
9
|
+
import { assertPathAllowed, fetchRemoteBytes, RemoteFetchError } from "./safe-io.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Client-side document conversion (anydoc engine).
|
|
@@ -104,6 +105,16 @@ export interface ConvertDocumentOptions {
|
|
|
104
105
|
maxChars?: number;
|
|
105
106
|
/** Abort signal honored by the remote-fetch leg. */
|
|
106
107
|
signal?: AbortSignal;
|
|
108
|
+
/**
|
|
109
|
+
* Allowed local roots for path inputs (see `AgentConfig.fileAccess`).
|
|
110
|
+
* Omitted = unrestricted (developer-supplied inputs); `[]` denies every path.
|
|
111
|
+
*/
|
|
112
|
+
roots?: readonly string[];
|
|
113
|
+
/**
|
|
114
|
+
* Input is model/user-controlled: private/internal hosts are refused for
|
|
115
|
+
* remote URLs. The built-in tool always sets this.
|
|
116
|
+
*/
|
|
117
|
+
untrusted?: boolean;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
/** Normalized conversion result (pre-XML-envelope). */
|
|
@@ -296,7 +307,23 @@ export async function convertDocumentInput(
|
|
|
296
307
|
try {
|
|
297
308
|
// Remote URL leg: fetch bytes first (anydoc reads files, not URLs).
|
|
298
309
|
if (typeof input === "string" && isHttpUrl(input)) {
|
|
299
|
-
|
|
310
|
+
let res;
|
|
311
|
+
try {
|
|
312
|
+
res = await fetchRemoteBytes(input, {
|
|
313
|
+
signal: opts.signal,
|
|
314
|
+
maxBytes: MAX_DOCUMENT_FETCH_BYTES,
|
|
315
|
+
allowPrivateNetwork: !opts.untrusted,
|
|
316
|
+
});
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (err instanceof RemoteFetchError) {
|
|
319
|
+
throw new DocumentConversionError(
|
|
320
|
+
err.reason === "too_large" ? "resourceLimit" : "blocked",
|
|
321
|
+
`[Agent Accelerator] Could not download document "${input}": ${err.message}`,
|
|
322
|
+
input
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
300
327
|
if (!res.ok) {
|
|
301
328
|
throw new DocumentConversionError(
|
|
302
329
|
"io",
|
|
@@ -304,19 +331,12 @@ export async function convertDocumentInput(
|
|
|
304
331
|
input
|
|
305
332
|
);
|
|
306
333
|
}
|
|
307
|
-
const buf =
|
|
308
|
-
if (buf.byteLength > MAX_DOCUMENT_FETCH_BYTES) {
|
|
309
|
-
throw new DocumentConversionError(
|
|
310
|
-
"resourceLimit",
|
|
311
|
-
`[Agent Accelerator] Remote document "${input}" is ${(buf.byteLength / 1_000_000).toFixed(1)} MB, over the ${MAX_DOCUMENT_FETCH_BYTES / 1_000_000} MB fetch cap.`,
|
|
312
|
-
input
|
|
313
|
-
);
|
|
314
|
-
}
|
|
334
|
+
const buf = res.bytes;
|
|
315
335
|
const name = filenameOpt || basenameOf(input);
|
|
316
336
|
const inferred = resolveDocumentFormat({
|
|
317
337
|
filename: name,
|
|
318
338
|
format: opts.format,
|
|
319
|
-
mimeType: res.
|
|
339
|
+
mimeType: res.contentType || opts.mimeType,
|
|
320
340
|
});
|
|
321
341
|
let wire = canonicalizeFormat(anydoc, inferred);
|
|
322
342
|
if (!wire) {
|
|
@@ -340,6 +360,17 @@ export async function convertDocumentInput(
|
|
|
340
360
|
|
|
341
361
|
// Local path leg: the engine reads + detects from the path itself.
|
|
342
362
|
if (typeof input === "string" && looksLikePath(input)) {
|
|
363
|
+
if (opts.roots) {
|
|
364
|
+
try {
|
|
365
|
+
await assertPathAllowed(input, opts.roots);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
throw new DocumentConversionError(
|
|
368
|
+
"blocked",
|
|
369
|
+
`[Agent Accelerator] Could not read document "${input.slice(0, 120)}": ${err instanceof Error ? err.message : String(err)}`,
|
|
370
|
+
input
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
343
374
|
const name = filenameOpt || basenameOf(input);
|
|
344
375
|
const markdown = await anydoc.toMarkdown(input);
|
|
345
376
|
const inferred = resolveDocumentFormat({ filename: name, format: opts.format });
|
|
@@ -424,7 +455,7 @@ export function modelSupportsFileInput(providerId: string, modelId: string): boo
|
|
|
424
455
|
*/
|
|
425
456
|
export async function preprocessFilePartsForBypass(
|
|
426
457
|
messages: Message[],
|
|
427
|
-
opts: { providerId: string; modelId: string; signal?: AbortSignal; maxChars?: number }
|
|
458
|
+
opts: { providerId: string; modelId: string; signal?: AbortSignal; maxChars?: number; roots?: readonly string[] }
|
|
428
459
|
): Promise<void> {
|
|
429
460
|
if (modelSupportsFileInput(opts.providerId, opts.modelId)) return;
|
|
430
461
|
for (const m of messages) {
|
|
@@ -451,6 +482,7 @@ export async function preprocessFilePartsForBypass(
|
|
|
451
482
|
mimeType,
|
|
452
483
|
maxChars: opts.maxChars,
|
|
453
484
|
signal: opts.signal,
|
|
485
|
+
roots: opts.roots,
|
|
454
486
|
});
|
|
455
487
|
const destination =
|
|
456
488
|
filename ||
|
|
@@ -466,8 +498,75 @@ export async function preprocessFilePartsForBypass(
|
|
|
466
498
|
}
|
|
467
499
|
}
|
|
468
500
|
|
|
501
|
+
/** Options for {@link createConvertDocumentTool}. */
|
|
502
|
+
export interface ConvertDocumentToolOptions {
|
|
503
|
+
/**
|
|
504
|
+
* Local roots the model may read from. Defaults to `[process.cwd()]`; pass
|
|
505
|
+
* `[]` to deny every local path (URLs only).
|
|
506
|
+
*/
|
|
507
|
+
roots?: readonly string[];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Builds the model-callable document converter. The `destination` is chosen
|
|
512
|
+
* by the model, so it is always treated as untrusted: local paths must sit
|
|
513
|
+
* inside `roots`, and remote URLs may not target private/internal hosts.
|
|
514
|
+
*
|
|
515
|
+
* @example `const tool = createConvertDocumentTool({ roots: ["./docs"] });`
|
|
516
|
+
*/
|
|
517
|
+
export function createConvertDocumentTool(options: ConvertDocumentToolOptions = {}): ToolDefinition {
|
|
518
|
+
return tool({
|
|
519
|
+
name: "convert_document_to_markdown",
|
|
520
|
+
description:
|
|
521
|
+
"Convert a document file to Markdown text. Call this when the user provides a document file path or URL, or asks about the contents of a document. " +
|
|
522
|
+
"Supported formats: PDF (.pdf), Word (.doc, .docx, .docm), PowerPoint (.ppt, .pps, .pot, .pptx, .pptm, .ppsx, .ppsm), " +
|
|
523
|
+
"Excel (.xls, .xlsx, .xlsm, .xlsb), OpenDocument (.odt, .ods, .odp), RTF (.rtf), EPUB (.epub), CSV (.csv). " +
|
|
524
|
+
"Returns the content wrapped in <Document name destination> tags. Do not use for images, audio, or video.",
|
|
525
|
+
input: z.object({
|
|
526
|
+
destination: z
|
|
527
|
+
.string()
|
|
528
|
+
.min(1)
|
|
529
|
+
.describe("Local file path or http(s) URL of the document to convert."),
|
|
530
|
+
format: z
|
|
531
|
+
.string()
|
|
532
|
+
.optional()
|
|
533
|
+
.describe(
|
|
534
|
+
"Explicit format override, e.g. 'pdf', 'docx', 'xlsx', 'csv'. Inferred from the destination when omitted."
|
|
535
|
+
),
|
|
536
|
+
maxChars: z
|
|
537
|
+
.number()
|
|
538
|
+
.int()
|
|
539
|
+
.positive()
|
|
540
|
+
.optional()
|
|
541
|
+
.describe("Maximum Markdown characters to return (default 100000). Excess is truncated with a marker."),
|
|
542
|
+
}),
|
|
543
|
+
timeoutMs: 60_000,
|
|
544
|
+
execute: async ({ destination, format, maxChars }, ctx) => {
|
|
545
|
+
const dest = String(destination ?? "").trim();
|
|
546
|
+
if (!dest) {
|
|
547
|
+
return "Error: convert_document_to_markdown needs a non-empty destination (local file path or http(s) URL).";
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const doc = await convertDocumentInput(dest, {
|
|
551
|
+
format,
|
|
552
|
+
maxChars,
|
|
553
|
+
signal: ctx?.signal,
|
|
554
|
+
untrusted: true,
|
|
555
|
+
roots: options.roots ?? [process.cwd()],
|
|
556
|
+
});
|
|
557
|
+
return buildDocumentXml(doc.name, dest, doc.markdown);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
560
|
+
return `Error: ${detail}`;
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
469
566
|
/**
|
|
470
|
-
* Built-in model-callable document converter
|
|
567
|
+
* Built-in model-callable document converter with the default policy (local
|
|
568
|
+
* paths limited to the working directory, no private-network URLs). Use
|
|
569
|
+
* {@link createConvertDocumentTool} to choose other roots.
|
|
471
570
|
*
|
|
472
571
|
* @example
|
|
473
572
|
* ```ts
|
|
@@ -475,43 +574,4 @@ export async function preprocessFilePartsForBypass(
|
|
|
475
574
|
* const agent = new Agent({ model, tools: { convert_document_to_markdown } });
|
|
476
575
|
* ```
|
|
477
576
|
*/
|
|
478
|
-
export const convert_document_to_markdown: ToolDefinition =
|
|
479
|
-
name: "convert_document_to_markdown",
|
|
480
|
-
description:
|
|
481
|
-
"Convert a document file to Markdown text. Call this when the user provides a document file path or URL, or asks about the contents of a document. " +
|
|
482
|
-
"Supported formats: PDF (.pdf), Word (.doc, .docx, .docm), PowerPoint (.ppt, .pps, .pot, .pptx, .pptm, .ppsx, .ppsm), " +
|
|
483
|
-
"Excel (.xls, .xlsx, .xlsm, .xlsb), OpenDocument (.odt, .ods, .odp), RTF (.rtf), EPUB (.epub), CSV (.csv). " +
|
|
484
|
-
"Returns the content wrapped in <Document name destination> tags. Do not use for images, audio, or video.",
|
|
485
|
-
input: z.object({
|
|
486
|
-
destination: z
|
|
487
|
-
.string()
|
|
488
|
-
.min(1)
|
|
489
|
-
.describe("Local file path or http(s) URL of the document to convert."),
|
|
490
|
-
format: z
|
|
491
|
-
.string()
|
|
492
|
-
.optional()
|
|
493
|
-
.describe(
|
|
494
|
-
"Explicit format override, e.g. 'pdf', 'docx', 'xlsx', 'csv'. Inferred from the destination when omitted."
|
|
495
|
-
),
|
|
496
|
-
maxChars: z
|
|
497
|
-
.number()
|
|
498
|
-
.int()
|
|
499
|
-
.positive()
|
|
500
|
-
.optional()
|
|
501
|
-
.describe("Maximum Markdown characters to return (default 100000). Excess is truncated with a marker."),
|
|
502
|
-
}),
|
|
503
|
-
timeoutMs: 60_000,
|
|
504
|
-
execute: async ({ destination, format, maxChars }, ctx) => {
|
|
505
|
-
const dest = String(destination ?? "").trim();
|
|
506
|
-
if (!dest) {
|
|
507
|
-
return "Error: convert_document_to_markdown needs a non-empty destination (local file path or http(s) URL).";
|
|
508
|
-
}
|
|
509
|
-
try {
|
|
510
|
-
const doc = await convertDocumentInput(dest, { format, maxChars, signal: ctx?.signal });
|
|
511
|
-
return buildDocumentXml(doc.name, dest, doc.markdown);
|
|
512
|
-
} catch (err) {
|
|
513
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
514
|
-
return `Error: ${detail}`;
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
});
|
|
577
|
+
export const convert_document_to_markdown: ToolDefinition = createConvertDocumentTool();
|
package/src/utils/env.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export function getEnv(key: string, fallback?: string): string | undefined {
|
|
11
11
|
if (typeof process !== "undefined" && process.env && process.env[key]) {
|
|
12
|
-
|
|
12
|
+
// Trim quotes/whitespace from .env files (`sk-..."\n` -> 401 otherwise).
|
|
13
|
+
const raw = String(process.env[key]);
|
|
14
|
+
const trimmed = raw.trim().replace(/^["']|["']$/g, "").trim();
|
|
15
|
+
if (trimmed) return trimmed;
|
|
13
16
|
}
|
|
14
17
|
return fallback;
|
|
15
18
|
}
|
|
@@ -22,20 +25,31 @@ export type ProviderEnv = Record<string, string>;
|
|
|
22
25
|
* @example `const key = getApiKey("google");`
|
|
23
26
|
*/
|
|
24
27
|
export function getApiKey(provider: string, explicitKey?: string, env?: ProviderEnv): string | undefined {
|
|
25
|
-
|
|
28
|
+
const clean = (v: unknown): string | undefined => {
|
|
29
|
+
if (typeof v !== "string") return undefined;
|
|
30
|
+
const t = v.trim().replace(/^["']|["']$/g, "").trim();
|
|
31
|
+
return t || undefined;
|
|
32
|
+
};
|
|
33
|
+
const cleanedExplicit = clean(explicitKey);
|
|
34
|
+
if (cleanedExplicit) return cleanedExplicit;
|
|
26
35
|
// ProviderEnv override takes precedence (agent-accel inspiration, SDK-light)
|
|
27
36
|
if (env) {
|
|
28
37
|
const upper = `${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
29
|
-
|
|
38
|
+
const fromEnv = clean(env[upper]);
|
|
39
|
+
if (fromEnv) return fromEnv;
|
|
30
40
|
// google aliases
|
|
31
41
|
if (provider.toLowerCase().startsWith("google") || provider.toLowerCase() === "gemini") {
|
|
32
|
-
|
|
33
|
-
if (
|
|
42
|
+
const g1 = clean(env["GEMINI_API_KEY"]);
|
|
43
|
+
if (g1) return g1;
|
|
44
|
+
const g2 = clean(env["GOOGLE_API_KEY"]);
|
|
45
|
+
if (g2) return g2;
|
|
34
46
|
}
|
|
35
47
|
// openai aliases
|
|
36
48
|
if (provider.toLowerCase() === "openai") {
|
|
37
|
-
|
|
38
|
-
if (
|
|
49
|
+
const o1 = clean(env["OPENAI_BASE_API_KEY"]);
|
|
50
|
+
if (o1) return o1;
|
|
51
|
+
const o2 = clean(env["OPENAI_API_KEY"]);
|
|
52
|
+
if (o2) return o2;
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
package/src/utils/errors.ts
CHANGED
|
@@ -54,7 +54,18 @@ export function toConciseProviderError(error: unknown, providerId: string, model
|
|
|
54
54
|
const err = error as any;
|
|
55
55
|
const status = readStatus(err);
|
|
56
56
|
const url = readUrl(err);
|
|
57
|
-
|
|
57
|
+
let detail = readProviderMessage(err);
|
|
58
|
+
// Append OpenRouter-style raw metadata once (pi api/openai-completions.ts:719
|
|
59
|
+
// rule): normalize already stringifies error.error, so only append raw when
|
|
60
|
+
// absent to avoid double-printing.
|
|
61
|
+
const rawMeta =
|
|
62
|
+
(typeof err?.raw === "string" && err.raw.trim() ? err.raw.trim() : undefined) ??
|
|
63
|
+
(typeof err?.error?.metadata?.raw === "string" && err.error.metadata.raw.trim()
|
|
64
|
+
? err.error.metadata.raw.trim()
|
|
65
|
+
: undefined);
|
|
66
|
+
if (rawMeta && !detail.includes(rawMeta.slice(0, 80))) {
|
|
67
|
+
detail = `${detail}\n${rawMeta.slice(0, 500)}`.slice(0, 800);
|
|
68
|
+
}
|
|
58
69
|
const concise = new Error(
|
|
59
70
|
`[${displayModel(providerId, modelId)}] request failed${status ? ` (${status})` : ""}: ${detail}`
|
|
60
71
|
);
|
|
@@ -75,6 +86,20 @@ export function toConciseProviderError(error: unknown, providerId: string, model
|
|
|
75
86
|
...(typeof err?.errorType === "string"
|
|
76
87
|
? { errorType: { value: err.errorType, enumerable: false } }
|
|
77
88
|
: {}),
|
|
89
|
+
// Forwarded when present (e.g. Google's `not_found` chain-eviction code,
|
|
90
|
+
// which carries no HTTP status on SSE error frames). Survives re-wraps
|
|
91
|
+
// so chain-expiry detectors never lose it.
|
|
92
|
+
...((typeof err?.code === "string" || typeof err?.code === "number") && err.code !== ""
|
|
93
|
+
? { code: { value: err.code, enumerable: false } }
|
|
94
|
+
: {}),
|
|
95
|
+
// Server rate-limit hint in ms (from Retry-After); survives re-wraps so
|
|
96
|
+
// `withRetries` can honor it instead of hammering a 429.
|
|
97
|
+
...(typeof err?.retryAfterMs === "number" && Number.isFinite(err.retryAfterMs)
|
|
98
|
+
? { retryAfterMs: { value: err.retryAfterMs, enumerable: false } }
|
|
99
|
+
: {}),
|
|
100
|
+
...(typeof (err as { raw?: unknown })?.raw === "string"
|
|
101
|
+
? { raw: { value: (err as { raw: string }).raw.slice(0, 500), enumerable: false } }
|
|
102
|
+
: {}),
|
|
78
103
|
});
|
|
79
104
|
return concise;
|
|
80
105
|
}
|
package/src/utils/headers.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function getPackageVersion(): string {
|
|
|
24
24
|
const v = (globalThis as any).process?.env?.npm_package_version;
|
|
25
25
|
if (typeof v === "string" && v) return v;
|
|
26
26
|
} catch {}
|
|
27
|
-
return "0.
|
|
27
|
+
return "0.5.0";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function getAgentAccelUserAgent(): string {
|
|
@@ -55,9 +55,9 @@ export interface AttributionConfig {
|
|
|
55
55
|
let attribution: AttributionConfig = {};
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* Configures global attribution headers
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* Configures global attribution headers (OpenRouter `HTTP-Referer` / `X-Title`).
|
|
59
|
+
* Defaults to none: nothing is sent unless you call this, so your traffic is
|
|
60
|
+
* never credited to a site or app you did not choose.
|
|
61
61
|
* @example `setAttribution({ referer: "https://example.com", title: "My App" });`
|
|
62
62
|
*/
|
|
63
63
|
export function setAttribution(cfg: AttributionConfig): void {
|
|
@@ -151,9 +151,8 @@ function stripForBrowser(headers: Record<string, string>): Record<string, string
|
|
|
151
151
|
/**
|
|
152
152
|
* Builds provider-specific session, cache-affinity, and attribution headers.
|
|
153
153
|
*
|
|
154
|
-
* Q-17: attribution is configurable via `setAttribution()` (default none
|
|
155
|
-
*
|
|
156
|
-
* fallback when no custom attribution is set. Custom (non-first-class)
|
|
154
|
+
* Q-17: attribution is configurable via `setAttribution()` (default none; no
|
|
155
|
+
* referer/title is ever sent unless you set one). Custom (non-first-class)
|
|
157
156
|
* prefixes receive minimal `x-session-id`/`x-client-request-id` affinity only
|
|
158
157
|
* (no `session_id` alias, no attribution) when a session id is present.
|
|
159
158
|
* `customHeaders` merge case-insensitively against canonical names.
|
|
@@ -193,14 +192,18 @@ export function buildSessionHeaders(
|
|
|
193
192
|
return key !== undefined ? headers[key] : undefined;
|
|
194
193
|
};
|
|
195
194
|
|
|
195
|
+
// OpenRouter app attribution is strictly opt-in (`setAttribution`): by default
|
|
196
|
+
// a user's traffic is not credited to any third-party app or site.
|
|
197
|
+
const applyOpenRouterAttribution = (): void => {
|
|
198
|
+
if (attr.referer !== undefined && getHeader("HTTP-Referer") === undefined) headers["HTTP-Referer"] = attr.referer;
|
|
199
|
+
if (attr.title !== undefined && getHeader("X-Title") === undefined) headers["X-Title"] = attr.title;
|
|
200
|
+
};
|
|
201
|
+
|
|
196
202
|
if (sessionId && !browser) {
|
|
197
203
|
if (provider === "openrouter") {
|
|
198
204
|
setHeaderCaseInsensitive(headers, "x-session-id", sessionId);
|
|
199
205
|
setHeaderCaseInsensitive(headers, "x-client-request-id", sessionId);
|
|
200
|
-
|
|
201
|
-
const title = attr.title ?? "Agent Accelerator";
|
|
202
|
-
if (getHeader("HTTP-Referer") === undefined) headers["HTTP-Referer"] = referer;
|
|
203
|
-
if (getHeader("X-Title") === undefined) headers["X-Title"] = title;
|
|
206
|
+
applyOpenRouterAttribution();
|
|
204
207
|
} else if (provider === "google") {
|
|
205
208
|
if (getHeader("x-goog-api-client") === undefined) {
|
|
206
209
|
headers["x-goog-api-client"] = "agent-accel/1.0";
|
|
@@ -219,10 +222,7 @@ export function buildSessionHeaders(
|
|
|
219
222
|
setHeaderCaseInsensitive(headers, "x-client-request-id", sessionId);
|
|
220
223
|
}
|
|
221
224
|
} else if (provider === "openrouter") {
|
|
222
|
-
|
|
223
|
-
const title = attr.title ?? "Agent Accelerator";
|
|
224
|
-
if (getHeader("HTTP-Referer") === undefined) headers["HTTP-Referer"] = referer;
|
|
225
|
-
if (getHeader("X-Title") === undefined) headers["X-Title"] = title;
|
|
225
|
+
applyOpenRouterAttribution();
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
if (provider === "google" && !browser && getHeader("x-goog-api-client") === undefined) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { getEnv } from "./env.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Agent Accelerator home: ONE folder that holds everything the SDK stores
|
|
6
|
+
* (`sessions/`, `scheduled_actions/`, `telemetry/`, `catalog/`), so nothing is
|
|
7
|
+
* scattered across the project or the package.
|
|
8
|
+
*/
|
|
9
|
+
let defaultHome: string | undefined;
|
|
10
|
+
const listeners = new Set<() => void>();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sets where the home lives for every Agent created afterwards, so nothing lands
|
|
14
|
+
* in the working directory unless you want it there. Call once at startup;
|
|
15
|
+
* `setHome()` clears it.
|
|
16
|
+
*/
|
|
17
|
+
export function setHome(dir?: string): void {
|
|
18
|
+
defaultHome = dir ? path.resolve(dir) : undefined;
|
|
19
|
+
for (const fn of listeners) fn();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The home folder: explicit `home`, else `setHome()`, else `AGENT_ACCEL_HOME`, else `.agent-accel/` in the working directory. */
|
|
23
|
+
export function resolveHome(home?: string): string {
|
|
24
|
+
return path.resolve(home || defaultHome || getEnv("AGENT_ACCEL_HOME") || ".agent-accel");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Runs `fn` whenever `setHome` changes the default (the catalog cache reloads from the new place). */
|
|
28
|
+
export function onHomeChange(fn: () => void): void {
|
|
29
|
+
listeners.add(fn);
|
|
30
|
+
}
|