agent-accelerator 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -112
- package/{SYSTEM_PROMPT_AGENT.md → examples/prompts/SYSTEM_PROMPT_AGENT.md} +1 -1
- package/package.json +14 -9
- package/src/agent/agent.ts +191 -71
- package/src/agent/context.ts +1 -0
- package/src/agent/delegation.ts +61 -12
- package/src/agent/loop.ts +71 -48
- package/src/data/README.md +6 -6
- package/src/index.ts +130 -45
- package/src/models/catalog-cache.ts +60 -9
- package/src/models/catalog.ts +53 -7
- package/src/providers/google.ts +926 -0
- package/src/providers/openai-compat.ts +1147 -0
- package/src/providers/openai.ts +959 -0
- package/src/providers/openrouter-responses.ts +949 -0
- package/src/providers/openrouter.ts +1037 -0
- package/src/{ai-sdk → providers}/registry.ts +43 -55
- package/src/providers.ts +490 -0
- package/src/streaming/sse-parser.ts +6 -4
- package/src/tools/executor.ts +19 -6
- package/src/tools/schema.ts +21 -11
- package/src/types/agent.ts +8 -1
- package/src/types/core.ts +1 -1
- package/src/types/message.ts +5 -0
- package/src/types/model.ts +3 -7
- package/src/types/provider-payloads.ts +2 -84
- package/src/types/tool.ts +6 -0
- package/src/update-models.ts +56 -0
- package/src/utils/cache.ts +1 -1
- package/src/utils/documents.ts +517 -0
- package/src/utils/env.ts +0 -7
- package/src/{ai-sdk → utils}/errors.ts +61 -2
- package/src/utils/headers.ts +10 -20
- package/src/utils/media.ts +5 -2
- package/src/utils/retry.ts +89 -0
- package/src/utils/serialization.ts +15 -0
- package/src/ai-sdk/converters.ts +0 -342
- package/src/ai-sdk/executor.ts +0 -454
- package/src/ai-sdk/index.ts +0 -55
- package/src/ai-sdk/model-provider.ts +0 -303
- package/src/ai-sdk/options.ts +0 -306
- package/src/ai-sdk/provider.ts +0 -415
- package/src/tokens/counter.ts +0 -136
- /package/{SYSTEM_PROMPT.md → examples/prompts/SYSTEM_PROMPT.md} +0 -0
- /package/{SYSTEM_PROMPT_TOOLS.md → examples/prompts/SYSTEM_PROMPT_TOOLS.md} +0 -0
package/src/agent/agent.ts
CHANGED
|
@@ -5,13 +5,26 @@ import type { ContentPart } from "../types/message.ts";
|
|
|
5
5
|
import { AgentResponse, type StreamEvent } from "../types/response.ts";
|
|
6
6
|
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
7
7
|
import { AgentContext } from "./context.ts";
|
|
8
|
-
import { resolveModel } from "../
|
|
8
|
+
import { resolveModel } from "../providers/registry.ts";
|
|
9
9
|
import { buildAgentTools, createSubagentSpawnTool } from "./delegation.ts";
|
|
10
|
+
import { convert_document_to_markdown } from "../utils/documents.ts";
|
|
10
11
|
import { runAgentLoop, streamAgentLoop } from "./loop.ts";
|
|
11
12
|
import { createSessionId } from "../utils/session.ts";
|
|
12
13
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
13
|
-
import { validateModelThinking } from "../models/catalog.ts";
|
|
14
|
-
|
|
14
|
+
import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalog.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves a per-run thinking override without mutating agent config.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveEffectiveThinking(
|
|
20
|
+
base?: ThinkingConfig,
|
|
21
|
+
overrideLevel?: string
|
|
22
|
+
): ThinkingConfig | undefined {
|
|
23
|
+
if (!overrideLevel) return base;
|
|
24
|
+
if (overrideLevel === "none") return { enabled: false, level: "none", budgetTokens: 0 };
|
|
25
|
+
if (overrideLevel === "dynamic") return { enabled: true, level: "dynamic", budgetTokens: -1 };
|
|
26
|
+
return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
|
|
27
|
+
}
|
|
15
28
|
|
|
16
29
|
/** Thrown when dynamic sub-agent spawning is enabled without an explicit model. */
|
|
17
30
|
export class SubAgentModelError extends Error {
|
|
@@ -91,6 +104,8 @@ export class Agent {
|
|
|
91
104
|
readonly cacheConfig?: CacheConfig;
|
|
92
105
|
/** Provider service tier. */
|
|
93
106
|
readonly serviceTier?: ServiceTier;
|
|
107
|
+
/** Convert `file` parts client-side for pdf-incapable models. */
|
|
108
|
+
readonly bypassInputFileModality: boolean;
|
|
94
109
|
/** Maximum model/tool turns per run. */
|
|
95
110
|
readonly maxTurns: number;
|
|
96
111
|
/** Session ID used for history and cache affinity. */
|
|
@@ -198,6 +213,7 @@ export class Agent {
|
|
|
198
213
|
const initialCachedId = (this.cacheConfig as any)?.cachedContentId;
|
|
199
214
|
|
|
200
215
|
this.serviceTier = config.serviceTier;
|
|
216
|
+
this.bypassInputFileModality = config.bypassInputFileModality ?? false;
|
|
201
217
|
this.stateless = config.stateless ?? false;
|
|
202
218
|
|
|
203
219
|
// Tools registration
|
|
@@ -225,6 +241,10 @@ export class Agent {
|
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
243
|
|
|
244
|
+
if (this.bypassInputFileModality && !this.tools["convert_document_to_markdown"]) {
|
|
245
|
+
this.tools["convert_document_to_markdown"] = convert_document_to_markdown;
|
|
246
|
+
}
|
|
247
|
+
|
|
228
248
|
if (this.dynamicSubagents?.enabled === true) {
|
|
229
249
|
if (!this.subagentModel) {
|
|
230
250
|
throw new SubAgentModelError(
|
|
@@ -288,6 +308,11 @@ export class Agent {
|
|
|
288
308
|
}
|
|
289
309
|
|
|
290
310
|
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions): void {
|
|
311
|
+
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
"[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
|
|
314
|
+
);
|
|
315
|
+
}
|
|
291
316
|
if (this.stateless) {
|
|
292
317
|
this.context.messages = [];
|
|
293
318
|
this.context.thoughtSignatures = [];
|
|
@@ -317,10 +342,26 @@ export class Agent {
|
|
|
317
342
|
* Runs one or more model/tool turns and resolves to a normalized AgentResponse.
|
|
318
343
|
* @example `const response = await agent.run("Summarize this document");`
|
|
319
344
|
*/
|
|
345
|
+
run(
|
|
346
|
+
prompt: string | ContentPart[],
|
|
347
|
+
options: AgentRunOptions & { stream: true } & (
|
|
348
|
+
| { onDelta: NonNullable<AgentRunOptions["onDelta"]> }
|
|
349
|
+
| { onThinkingDelta: NonNullable<AgentRunOptions["onThinkingDelta"]> }
|
|
350
|
+
| { onEvent: NonNullable<AgentRunOptions["onEvent"]> }
|
|
351
|
+
)
|
|
352
|
+
): Promise<AgentResponse> & AsyncIterable<StreamEvent>;
|
|
353
|
+
run(
|
|
354
|
+
prompt: string | ContentPart[],
|
|
355
|
+
options: AgentRunOptions & { stream: true }
|
|
356
|
+
): AssistantMessageEventStream;
|
|
357
|
+
run(
|
|
358
|
+
prompt: string | ContentPart[],
|
|
359
|
+
options?: AgentRunOptions
|
|
360
|
+
): Promise<AgentResponse>;
|
|
320
361
|
run(
|
|
321
362
|
prompt: string | ContentPart[],
|
|
322
363
|
options?: AgentRunOptions
|
|
323
|
-
): Promise<AgentResponse>
|
|
364
|
+
): Promise<AgentResponse> | AssistantMessageEventStream {
|
|
324
365
|
const isStream = options?.stream === true;
|
|
325
366
|
if (isStream) {
|
|
326
367
|
const hasCallbacks = !!(options?.onDelta || options?.onThinkingDelta || options?.onEvent);
|
|
@@ -343,44 +384,48 @@ export class Agent {
|
|
|
343
384
|
return s;
|
|
344
385
|
}
|
|
345
386
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
: { ...this.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
387
|
+
return (async (): Promise<AgentResponse> => {
|
|
388
|
+
// Refresh the catalog BEFORE thinking validation so levels are checked
|
|
389
|
+
// against live data instead of a possibly-empty cold cache.
|
|
390
|
+
await ensureModelCatalogFresh();
|
|
391
|
+
const resolved = resolveModel(this.modelStringOrSpec);
|
|
392
|
+
const effectiveLevel = options?.thinkingLevel || this.thinkingConfig?.level;
|
|
393
|
+
if (effectiveLevel) {
|
|
394
|
+
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
395
|
+
}
|
|
396
|
+
this.prepareTurn(prompt, options);
|
|
397
|
+
|
|
398
|
+
const providerOptions = {
|
|
399
|
+
apiKey: this.apiKey,
|
|
400
|
+
baseUrl: this.baseUrl,
|
|
401
|
+
headers: { ...(this.customHeaders ?? {}), ...(options?.headers ?? {}) },
|
|
402
|
+
thinking: resolveEffectiveThinking(this.thinkingConfig, options?.thinkingLevel),
|
|
403
|
+
cache: this.stateless
|
|
404
|
+
? { sessionId: options?.sessionId || this.sessionId }
|
|
405
|
+
: { ...this.cacheConfig, sessionId: options?.sessionId || this.sessionId },
|
|
406
|
+
serviceTier: this.serviceTier,
|
|
407
|
+
sessionId: options?.sessionId || this.sessionId,
|
|
408
|
+
};
|
|
364
409
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
410
|
+
const loopConfig = {
|
|
411
|
+
agentName: this.name,
|
|
412
|
+
provider: resolved.provider,
|
|
413
|
+
modelId: resolved.modelId,
|
|
414
|
+
context: this.context,
|
|
415
|
+
tools: this.tools,
|
|
416
|
+
options: providerOptions,
|
|
417
|
+
runOptions: options,
|
|
418
|
+
maxTurns: this.maxTurns,
|
|
419
|
+
bypassInputFileModality: this.bypassInputFileModality,
|
|
420
|
+
};
|
|
375
421
|
|
|
376
|
-
|
|
422
|
+
const res = await runAgentLoop(loopConfig);
|
|
377
423
|
if (this.stateless) {
|
|
378
424
|
this.context.messages = [];
|
|
379
425
|
this.context.thoughtSignatures = [];
|
|
380
426
|
}
|
|
381
427
|
return res;
|
|
382
|
-
});
|
|
383
|
-
return promise as any;
|
|
428
|
+
})();
|
|
384
429
|
}
|
|
385
430
|
|
|
386
431
|
/**
|
|
@@ -409,10 +454,16 @@ export class Agent {
|
|
|
409
454
|
}
|
|
410
455
|
const stringStream: any = {
|
|
411
456
|
[Symbol.asyncIterator]: async function* () {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
457
|
+
try {
|
|
458
|
+
for await (const chunk of stream) {
|
|
459
|
+
if (chunk.type === "text_delta" && chunk.delta) {
|
|
460
|
+
yield chunk.delta;
|
|
461
|
+
}
|
|
415
462
|
}
|
|
463
|
+
} finally {
|
|
464
|
+
// Breaking out early must stop the underlying provider stream
|
|
465
|
+
// instead of leaving the HTTP connection running in the background.
|
|
466
|
+
stream.cancel();
|
|
416
467
|
}
|
|
417
468
|
},
|
|
418
469
|
result: () => stream.result(),
|
|
@@ -428,7 +479,7 @@ export class Agent {
|
|
|
428
479
|
return stringStream;
|
|
429
480
|
}
|
|
430
481
|
|
|
431
|
-
return this.run(prompt, runOpts);
|
|
482
|
+
return this.run(prompt, runOpts) as any;
|
|
432
483
|
}
|
|
433
484
|
|
|
434
485
|
/**
|
|
@@ -472,6 +523,7 @@ export class Agent {
|
|
|
472
523
|
options: providerOptions,
|
|
473
524
|
runOptions: options,
|
|
474
525
|
maxTurns: this.maxTurns,
|
|
526
|
+
bypassInputFileModality: this.bypassInputFileModality,
|
|
475
527
|
};
|
|
476
528
|
|
|
477
529
|
const s = streamAgentLoop(loopConfig);
|
|
@@ -483,8 +535,32 @@ export class Agent {
|
|
|
483
535
|
}
|
|
484
536
|
// Wire one-liner callbacks so `stream:true` + onDelta is enough — no manual for-await needed
|
|
485
537
|
if (options?.wrapThinking) {
|
|
486
|
-
// Auto-wrap reasoning as <think
|
|
538
|
+
// Auto-wrap reasoning as <think>…</think> per-turn — clean boundaries across multi-turn agent runs.
|
|
539
|
+
//
|
|
540
|
+
// Display-only whitespace hygiene (cache-safe): provider thought
|
|
541
|
+
// summaries routinely carry edge blank lines (observed `"...\n\n\n"`),
|
|
542
|
+
// and the wrapper itself adds newlines around the span. Without care
|
|
543
|
+
// this renders as blank lines after `<think>`, double blanks between
|
|
544
|
+
// sections, and a stray blank line before `</think>`. The buffering
|
|
545
|
+
// below touches ONLY the presented strings/callback args and synthetic
|
|
546
|
+
// tag events — never provider accumulation, AgentContext history, or
|
|
547
|
+
// request bodies — so prefix-cache affinity is untouched.
|
|
548
|
+
//
|
|
549
|
+
// Trailing-newline buffering: each chunk's trailing `\n` run is
|
|
550
|
+
// withheld and prepended to the next chunk (or dropped at close), so
|
|
551
|
+
// emitted display text never ends with `\n` and `</think>` never gets
|
|
552
|
+
// a preceding blank line. Interior breaks are preserved (collapsed to
|
|
553
|
+
// a single blank line at most).
|
|
487
554
|
let isThinking = false;
|
|
555
|
+
let emittedAny = false;
|
|
556
|
+
let pendingNewlines = "";
|
|
557
|
+
// True once text has been emitted without a trailing newline: the next
|
|
558
|
+
// <think> open tag then needs its own leading newline, otherwise it
|
|
559
|
+
// glues onto the streamed text mid-line. False at stream start and
|
|
560
|
+
// after every tag (both end with newlines), preserving the exact
|
|
561
|
+
// "<think>\n...\n</think>\n\n" contract for thinking-first streams.
|
|
562
|
+
let needThinkNewline = false;
|
|
563
|
+
const collapseBlankLines = (s: string): string => s.replace(/\n{3,}/g, "\n\n");
|
|
488
564
|
const userOnThinking = options.onThinkingDelta;
|
|
489
565
|
const userOnDelta = options.onDelta;
|
|
490
566
|
const userOnEvent = options.onEvent;
|
|
@@ -493,7 +569,10 @@ export class Agent {
|
|
|
493
569
|
const openThink = (e?: any) => {
|
|
494
570
|
if (!isThinking) {
|
|
495
571
|
isThinking = true;
|
|
496
|
-
|
|
572
|
+
emittedAny = false;
|
|
573
|
+
pendingNewlines = "";
|
|
574
|
+
const tag = (needThinkNewline ? "\n" : "") + "<think>\n";
|
|
575
|
+
needThinkNewline = false;
|
|
497
576
|
if (userOnThinking) userOnThinking(tag, e);
|
|
498
577
|
else if (userOnDelta) userOnDelta(tag, e as any);
|
|
499
578
|
}
|
|
@@ -502,20 +581,54 @@ export class Agent {
|
|
|
502
581
|
const closeThink = (e?: any) => {
|
|
503
582
|
if (isThinking) {
|
|
504
583
|
isThinking = false;
|
|
505
|
-
|
|
584
|
+
// Drop any withheld trailing newlines: the span ends cleanly and
|
|
585
|
+
// the tag supplies its own line breaks. Empty spans still close.
|
|
586
|
+
pendingNewlines = "";
|
|
587
|
+
const close = emittedAny ? "\n</think>\n\n" : "</think>\n\n";
|
|
588
|
+
emittedAny = false;
|
|
506
589
|
if (userOnThinking) userOnThinking(close, e);
|
|
507
590
|
else if (userOnDelta) userOnDelta(close, e as any);
|
|
508
591
|
}
|
|
509
592
|
};
|
|
510
593
|
|
|
594
|
+
/** Buffers one raw thinking chunk, returning the display string to
|
|
595
|
+
* emit now (`""` when nothing displayable yet). Mutates the event in
|
|
596
|
+
* place so manual `for-await` consumers see the same clean text; safe
|
|
597
|
+
* because loop context derives from the provider result, not these
|
|
598
|
+
* outer events. */
|
|
599
|
+
const bufferThinkingEvent = (e: any): string => {
|
|
600
|
+
const raw: string = e.thinkingDelta ?? "";
|
|
601
|
+
let chunk = collapseBlankLines(pendingNewlines + raw);
|
|
602
|
+
pendingNewlines = "";
|
|
603
|
+
if (!emittedAny) {
|
|
604
|
+
chunk = chunk.replace(/^\n+/, "");
|
|
605
|
+
}
|
|
606
|
+
if (!chunk) return "";
|
|
607
|
+
const m = chunk.match(/\n+$/);
|
|
608
|
+
if (m) {
|
|
609
|
+
pendingNewlines = m[0];
|
|
610
|
+
chunk = chunk.slice(0, -m[0].length);
|
|
611
|
+
if (!chunk) return "";
|
|
612
|
+
}
|
|
613
|
+
emittedAny = true;
|
|
614
|
+
e.thinkingDelta = chunk;
|
|
615
|
+
if (typeof e.partialThinking === "string") {
|
|
616
|
+
e.partialThinking = collapseBlankLines(e.partialThinking);
|
|
617
|
+
}
|
|
618
|
+
return chunk;
|
|
619
|
+
};
|
|
620
|
+
|
|
511
621
|
if (userOnThinking || userOnDelta) {
|
|
512
622
|
s.on("thinking_delta", (e: any) => {
|
|
513
623
|
openThink(e);
|
|
514
|
-
|
|
515
|
-
|
|
624
|
+
const chunk = bufferThinkingEvent(e);
|
|
625
|
+
if (!chunk) return;
|
|
626
|
+
if (userOnThinking) userOnThinking(chunk, e);
|
|
627
|
+
else if (userOnDelta) userOnDelta(chunk, e as any);
|
|
516
628
|
});
|
|
517
629
|
s.on("text_delta", (e: any) => {
|
|
518
630
|
closeThink(e);
|
|
631
|
+
needThinkNewline = !((e.delta ?? "").endsWith("\n"));
|
|
519
632
|
if (userOnDelta) userOnDelta(e.delta!, e);
|
|
520
633
|
else if (userOnThinking) userOnThinking(e.delta!, e as any);
|
|
521
634
|
});
|
|
@@ -528,48 +641,55 @@ export class Agent {
|
|
|
528
641
|
closeThink(e || ({ type: "done" } as any));
|
|
529
642
|
});
|
|
530
643
|
} else {
|
|
531
|
-
// No callbacks but wrapThinking true — still emit tags as events for manual iteration
|
|
644
|
+
// No callbacks but wrapThinking true — still emit tags as events for manual iteration.
|
|
645
|
+
// Same display hygiene as above (in-place event normalization is safe:
|
|
646
|
+
// loop context derives from the provider result, not outer events).
|
|
647
|
+
const closeSynthetic = () => {
|
|
648
|
+
if (isThinking) {
|
|
649
|
+
isThinking = false;
|
|
650
|
+
pendingNewlines = "";
|
|
651
|
+
const tag = emittedAny ? "\n</think>\n\n" : "</think>\n\n";
|
|
652
|
+
emittedAny = false;
|
|
653
|
+
s.push({ type: "thinking_delta", thinkingDelta: tag, __synthetic: true } as any);
|
|
654
|
+
}
|
|
655
|
+
};
|
|
532
656
|
s.on("thinking_delta", (e: any) => {
|
|
657
|
+
// Ignore our own synthetic tags: push() redelivers events to
|
|
658
|
+
// listeners synchronously, so without this guard the handler
|
|
659
|
+
// re-enters on every tag it emits (duplicate <think> spans and
|
|
660
|
+
// mutated queued events).
|
|
661
|
+
if ((e as any).__synthetic) return;
|
|
533
662
|
if (!isThinking) {
|
|
534
663
|
isThinking = true;
|
|
535
|
-
|
|
664
|
+
emittedAny = false;
|
|
665
|
+
pendingNewlines = "";
|
|
666
|
+
const openTag = (needThinkNewline ? "\n" : "") + "<think>\n";
|
|
667
|
+
needThinkNewline = false;
|
|
668
|
+
s.push({ type: "thinking_delta", thinkingDelta: openTag, __synthetic: true } as any);
|
|
669
|
+
}
|
|
670
|
+
const chunk = bufferThinkingEvent(e);
|
|
671
|
+
if (!chunk) {
|
|
672
|
+
e.thinkingDelta = "";
|
|
536
673
|
}
|
|
537
674
|
});
|
|
538
675
|
s.on("text_delta", (e: any) => {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
542
|
-
}
|
|
676
|
+
closeSynthetic();
|
|
677
|
+
needThinkNewline = !((e.delta ?? "").endsWith("\n"));
|
|
543
678
|
});
|
|
544
679
|
s.on("tool_call_start" as any, () => {
|
|
545
|
-
|
|
546
|
-
isThinking = false;
|
|
547
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
548
|
-
}
|
|
680
|
+
closeSynthetic();
|
|
549
681
|
});
|
|
550
682
|
s.on("tool_call_complete" as any, () => {
|
|
551
|
-
|
|
552
|
-
isThinking = false;
|
|
553
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
554
|
-
}
|
|
683
|
+
closeSynthetic();
|
|
555
684
|
});
|
|
556
685
|
s.on("tool_result" as any, () => {
|
|
557
|
-
|
|
558
|
-
isThinking = false;
|
|
559
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
560
|
-
}
|
|
686
|
+
closeSynthetic();
|
|
561
687
|
});
|
|
562
688
|
s.on("subagent_complete" as any, () => {
|
|
563
|
-
|
|
564
|
-
isThinking = false;
|
|
565
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
566
|
-
}
|
|
689
|
+
closeSynthetic();
|
|
567
690
|
});
|
|
568
691
|
s.on("done", () => {
|
|
569
|
-
|
|
570
|
-
isThinking = false;
|
|
571
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
572
|
-
}
|
|
692
|
+
closeSynthetic();
|
|
573
693
|
});
|
|
574
694
|
if (userOnEvent) s.on("*", userOnEvent as any);
|
|
575
695
|
}
|
|
@@ -598,7 +718,7 @@ export function normalizeThinking(config: AgentConfig, fallbackLevel?: string):
|
|
|
598
718
|
const provStr = typeof rawModel === "object" && rawModel?.provider ? rawModel.provider : modelStr.includes("/") ? modelStr.split("/")[0] : "";
|
|
599
719
|
const actualModelId = modelStr.includes("/") ? modelStr.split("/").slice(1).join("/") : modelStr;
|
|
600
720
|
if (actualModelId) {
|
|
601
|
-
validateModelThinking(provStr || "
|
|
721
|
+
validateModelThinking(provStr || "openrouter", actualModelId, level);
|
|
602
722
|
}
|
|
603
723
|
} catch (e) {
|
|
604
724
|
throw e;
|
package/src/agent/context.ts
CHANGED
package/src/agent/delegation.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { tool } from "../tools/tool.ts";
|
|
3
|
+
import { normalizeToolName } from "../tools/executor.ts";
|
|
3
4
|
import type { ToolDefinition } from "../types/tool.ts";
|
|
4
5
|
import type { SubAgentExecutionMetadata } from "../types/response.ts";
|
|
5
6
|
import type { Agent } from "./agent.ts";
|
|
6
|
-
import { resolveModel } from "../
|
|
7
|
+
import { resolveModel } from "../providers/registry.ts";
|
|
7
8
|
|
|
8
9
|
/** Task descriptor accepted by the automatic `spawn_subagents` tool.
|
|
9
10
|
*
|
|
@@ -58,7 +59,27 @@ function createChildSessionId(parentId: string, tag: string): string {
|
|
|
58
59
|
} catch {
|
|
59
60
|
rand = Math.random().toString(36).slice(2, 10);
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
+
// Provider-safe: OpenAI `prompt_cache_key` enforces max 64 chars (400
|
|
63
|
+
// otherwise). Parent ids are already ~42 chars (`accel-<uuid>`), so naive
|
|
64
|
+
// `${parent}-sub-${tag}-${rand}` overflows (observed 65-72 chars → every
|
|
65
|
+
// sub-agent 400s with 0 usage). Truncate the parent portion to fit, keeping
|
|
66
|
+
// the tag + rand suffix intact for uniqueness/debuggability.
|
|
67
|
+
const cleanTag = tag.toLowerCase().slice(0, 16);
|
|
68
|
+
const suffix = `-sub-${cleanTag}-${rand}`;
|
|
69
|
+
const maxParent = Math.max(0, 64 - suffix.length);
|
|
70
|
+
const truncatedParent = parentId.slice(0, maxParent);
|
|
71
|
+
return `${truncatedParent}${suffix}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Builds a deterministic fixed-subagent session id that fits 64 chars. */
|
|
75
|
+
function createFixedChildSessionId(parentId: string, name: string): string {
|
|
76
|
+
const suffix = `-sub-${name}`;
|
|
77
|
+
if ((parentId + suffix).length <= 64) return parentId + suffix;
|
|
78
|
+
// Truncate parent first (preserves full tool name for debugging); if still
|
|
79
|
+
// over (very long tool name), truncate the name tail as last resort.
|
|
80
|
+
const maxParent = Math.max(0, 64 - suffix.length);
|
|
81
|
+
if (maxParent > 0) return parentId.slice(0, maxParent) + suffix;
|
|
82
|
+
return (`${parentId}-sub-${name}`).slice(0, 64);
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
function providerOf(modelStr: string | any | undefined): string | undefined {
|
|
@@ -247,10 +268,18 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
247
268
|
if (context?.signal?.aborted) throw new Error("Aborted before spawn");
|
|
248
269
|
const childSessionId = createChildSessionId(parentAgent.sessionId, subagentName);
|
|
249
270
|
// Grant ONLY the Main Agent-selected subset from the developer pool. Unknown names are dropped.
|
|
271
|
+
// Matching is case/format-insensitive (same rules as tool execution): the model may emit
|
|
272
|
+
// "RECENT_NEWS" or "recent news" for a registered "recent_news" tool.
|
|
273
|
+
const poolByNormalized = new Map<string, ToolDefinition>();
|
|
274
|
+
for (const [key, def] of Object.entries(toolPool)) {
|
|
275
|
+
poolByNormalized.set(normalizeToolName(key), def);
|
|
276
|
+
const declared = (def as ToolDefinition)?.name;
|
|
277
|
+
if (declared) poolByNormalized.set(normalizeToolName(declared), def);
|
|
278
|
+
}
|
|
250
279
|
const grantedTools: Record<string, ToolDefinition> = {};
|
|
251
280
|
for (const toolName of (t as any).tools ?? []) {
|
|
252
|
-
const pooled = toolPool[toolName];
|
|
253
|
-
if (pooled) grantedTools[toolName] = pooled;
|
|
281
|
+
const pooled = toolPool[toolName] ?? poolByNormalized.get(normalizeToolName(String(toolName)));
|
|
282
|
+
if (pooled) grantedTools[(pooled as ToolDefinition).name || toolName] = pooled;
|
|
254
283
|
}
|
|
255
284
|
// Timeout: >0 fixed for every worker; 0 = no limit; -1 = per-task timeoutMs from the Main Agent.
|
|
256
285
|
const workerTimeout = cfgTimeout > 0 ? cfgTimeout : cfgTimeout === -1 && (t as any).timeoutMs > 0 ? (t as any).timeoutMs : 0;
|
|
@@ -271,21 +300,40 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
271
300
|
maxTurns: parentAgent.maxTurns,
|
|
272
301
|
});
|
|
273
302
|
let abortListener: (() => void) | null = null;
|
|
274
|
-
|
|
303
|
+
// Per-worker controller: the parent signal alone cannot stop a
|
|
304
|
+
// worker that hits its own timeout, so the timeout path aborts
|
|
305
|
+
// the worker explicitly instead of leaving it running unseen.
|
|
306
|
+
const workerController = new AbortController();
|
|
307
|
+
const parentSignal = context?.signal;
|
|
308
|
+
const forwardParentAbort = () => {
|
|
309
|
+
try {
|
|
310
|
+
workerController.abort((parentSignal as any)?.reason);
|
|
311
|
+
} catch {
|
|
312
|
+
try { workerController.abort(); } catch {}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
if (parentSignal?.aborted) forwardParentAbort();
|
|
316
|
+
else parentSignal?.addEventListener("abort", forwardParentAbort, { once: true });
|
|
317
|
+
const abortPromise = parentSignal
|
|
275
318
|
? new Promise<never>((_, reject) => {
|
|
276
319
|
const onAbort = () => reject(new Error("Sub-agent aborted via parent signal"));
|
|
277
320
|
abortListener = onAbort;
|
|
278
|
-
if (
|
|
279
|
-
else
|
|
321
|
+
if (parentSignal.aborted) reject(new Error("Sub-agent aborted via parent signal"));
|
|
322
|
+
else parentSignal.addEventListener("abort", onAbort, { once: true });
|
|
280
323
|
})
|
|
281
324
|
: null;
|
|
282
325
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
283
326
|
const timeoutPromise = workerTimeout > 0
|
|
284
327
|
? new Promise<never>((_, reject) => {
|
|
285
|
-
timeoutId = setTimeout(() =>
|
|
328
|
+
timeoutId = setTimeout(() => {
|
|
329
|
+
try {
|
|
330
|
+
workerController.abort(new Error(`Sub-agent ${subagentName} timed out after ${workerTimeout}ms`));
|
|
331
|
+
} catch {}
|
|
332
|
+
reject(new Error(`Sub-agent ${subagentName} timed out after ${workerTimeout}ms`));
|
|
333
|
+
}, workerTimeout);
|
|
286
334
|
})
|
|
287
335
|
: null;
|
|
288
|
-
const taskPromise = subAgent.run(t.task, { signal:
|
|
336
|
+
const taskPromise = subAgent.run(t.task, { signal: workerController.signal } as any);
|
|
289
337
|
try {
|
|
290
338
|
const racers: Promise<unknown>[] = [taskPromise as unknown as Promise<unknown>];
|
|
291
339
|
if (abortPromise) racers.push(abortPromise);
|
|
@@ -294,9 +342,10 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
294
342
|
return res;
|
|
295
343
|
} finally {
|
|
296
344
|
if (timeoutId) clearTimeout(timeoutId);
|
|
297
|
-
if (abortListener &&
|
|
298
|
-
try {
|
|
345
|
+
if (abortListener && parentSignal) {
|
|
346
|
+
try { parentSignal.removeEventListener("abort", abortListener as any); } catch {}
|
|
299
347
|
}
|
|
348
|
+
try { parentSignal?.removeEventListener("abort", forwardParentAbort); } catch {}
|
|
300
349
|
}
|
|
301
350
|
};
|
|
302
351
|
|
|
@@ -404,7 +453,7 @@ export function agentToTool(
|
|
|
404
453
|
const startTime = Date.now();
|
|
405
454
|
try {
|
|
406
455
|
const parentSessionId = ctx?.sessionId || agentInstance.sessionId || "session";
|
|
407
|
-
const subSessionId =
|
|
456
|
+
const subSessionId = createFixedChildSessionId(parentSessionId, sanitizeToolName(name));
|
|
408
457
|
const response: any = await agentInstance.run(task, { signal: ctx?.signal, sessionId: subSessionId } as any);
|
|
409
458
|
const durationMs = Date.now() - startTime;
|
|
410
459
|
const metadata: SubAgentExecutionMetadata = {
|