@jini-ai/daemon 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-executor/contracts.d.ts +4 -1
- package/dist/agent-executor/event-translation.js +4 -0
- package/dist/agent-executor/events.js +99 -33
- package/dist/agent-executor/index.d.ts +1 -1
- package/dist/agent-executor/index.js +1 -1
- package/dist/agent-executor/launch.d.ts +11 -2
- package/dist/agent-executor/launch.js +37 -13
- package/dist/agent-executor.d.ts +1 -1
- package/dist/agent-executor.js +1 -1
- package/dist/http/index.d.ts +3 -2
- package/dist/http/index.js +2 -1
- package/dist/http/research-tavily.adapter.d.ts +6 -0
- package/dist/http/research-tavily.adapter.js +109 -0
- package/dist/http/research.d.ts +20 -2
- package/dist/http/research.js +64 -149
- package/package.json +13 -13
|
@@ -2,7 +2,7 @@ import type { MessageAttachmentImage } from '../attachment-content.js';
|
|
|
2
2
|
import type { spawn as nodeSpawn, ChildProcess } from 'node:child_process';
|
|
3
3
|
import type { Principal } from '@jini-ai/core';
|
|
4
4
|
import type { RunAgentPayload, RunErrorPayload } from '@jini-ai/protocol';
|
|
5
|
-
import type { applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, AcpMcpServerInput, AcpPermissionHandler, PromptAugmenter } from '@jini-ai/agent-runtime';
|
|
5
|
+
import type { AgentRuntimeState, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, AcpMcpServerInput, AcpPermissionHandler, PromptAugmenter } from '@jini-ai/agent-runtime';
|
|
6
6
|
import type { createCommandInvocation, listProcessSnapshots, ProcessSnapshot, StopProcessesResult } from '@jini-ai/platform';
|
|
7
7
|
import type { RunByteJournal } from '../continuation/journal.js';
|
|
8
8
|
import type { RunRetrySideEffectState } from '../run/core/index.js';
|
|
@@ -571,6 +571,9 @@ export interface FailureClassificationContext {
|
|
|
571
571
|
*/
|
|
572
572
|
export type ClassifyFailure = (context: FailureClassificationContext) => boolean | Promise<boolean>;
|
|
573
573
|
export interface CreateAgentExecutorOptions {
|
|
574
|
+
/** Host environment snapshot; baseline spawn keys and source config paths are allowlisted. */
|
|
575
|
+
readonly hostEnv?: NodeJS.ProcessEnv;
|
|
576
|
+
readonly runtimeState?: AgentRuntimeState;
|
|
574
577
|
readonly lifecycle: RunLifecycle;
|
|
575
578
|
/** Resolves/pins the concrete starting model against the actual launch scope. */
|
|
576
579
|
readonly resolveModelForLaunch?: typeof resolveModelForLaunch;
|
|
@@ -39,11 +39,15 @@ function translateUsagePayload(rawEvent) {
|
|
|
39
39
|
const rawUsage = isRecord(rawEvent.usage) ? rawEvent.usage : undefined;
|
|
40
40
|
const usage = extractUsageTokens({ rawUsage: rawUsage });
|
|
41
41
|
const costUsd = asOptionalNumber(rawEvent.costUsd);
|
|
42
|
+
const cumulativeCostUsd = asOptionalNumber(rawEvent.cumulativeCostUsd);
|
|
43
|
+
const costSessionId = asOptionalString(rawEvent.costSessionId);
|
|
42
44
|
const durationMs = asOptionalNumber(rawEvent.durationMs);
|
|
43
45
|
return {
|
|
44
46
|
type: 'usage',
|
|
45
47
|
...(usage !== undefined ? { usage } : {}),
|
|
46
48
|
...(costUsd !== undefined ? { costUsd } : {}),
|
|
49
|
+
...(cumulativeCostUsd !== undefined ? { cumulativeCostUsd } : {}),
|
|
50
|
+
...(costSessionId !== undefined ? { costSessionId } : {}),
|
|
47
51
|
...(durationMs !== undefined ? { durationMs } : {}),
|
|
48
52
|
};
|
|
49
53
|
}
|
|
@@ -26,7 +26,7 @@ function statusWithChildIdentity({ payload, childPid }, _optional = {}) {
|
|
|
26
26
|
* parser's own documented behavior. Never called for `streamFormat:
|
|
27
27
|
* 'plain'` — `wireChildLifecycle` handles that format inline with no
|
|
28
28
|
* parser at all (see `ChildDrivenStreamFormat`'s doc).
|
|
29
|
-
* @param def - The resolved agent def (
|
|
29
|
+
* @param def - The resolved agent def (`.eventParser` and `.usageCostBasis` configure parser behavior).
|
|
30
30
|
* @param streamFormat - `def.streamFormat`, already narrowed by {@link isSupportedStreamFormat}.
|
|
31
31
|
* @param onEvent - Sink the parser calls once per parsed (or malformed-raw) event.
|
|
32
32
|
* @returns A `{feed, flush}` handle for the chosen parser.
|
|
@@ -36,7 +36,7 @@ function statusWithChildIdentity({ payload, childPid }, _optional = {}) {
|
|
|
36
36
|
function createStreamHandlerForDef(def, streamFormat, onEvent) {
|
|
37
37
|
switch (streamFormat) {
|
|
38
38
|
case 'claude-stream-json':
|
|
39
|
-
return createClaudeStreamHandler({ onEvent: onEvent });
|
|
39
|
+
return createClaudeStreamHandler({ onEvent: onEvent }, { costBasis: def.usageCostBasis ?? 'run' });
|
|
40
40
|
case 'copilot-stream-json':
|
|
41
41
|
return createCopilotStreamHandler({ onEvent: onEvent });
|
|
42
42
|
case 'qoder-stream-json':
|
|
@@ -350,27 +350,63 @@ export function wireChildLifecycle(ctx) {
|
|
|
350
350
|
// Set once this run has interrupted its agent: from then on a turn the interrupt aborted
|
|
351
351
|
// (`aborted_*` stop reason) is not the run's last — the message that follows starts a new one.
|
|
352
352
|
let interruptSent = false;
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
353
|
+
// Claude Code's background tasks still running (a subagent or shell it started in the
|
|
354
|
+
// background), from its latest `background_tasks_changed` line, which lists them all. While any
|
|
355
|
+
// runs, an `end_turn` is not the run's end: the CLI keeps going until they finish and reports on
|
|
356
|
+
// them, so stdin must stay open for mid-run messages. Closing it there (2026-10-10, plannie chat)
|
|
357
|
+
// made every message sent while two subagents worked come back 'not-running' and wait for the run
|
|
358
|
+
// to end — and at EOF the CLI stops background tasks at its own wait ceiling (10 minutes).
|
|
359
|
+
let backgroundTaskCount = 0;
|
|
360
|
+
// Set when a turn ended and stdin stayed open only for background tasks: no turn is running, so a
|
|
361
|
+
// mid-run message has nothing to interrupt, and the last task finishing closes stdin (below).
|
|
362
|
+
let heldForBackgroundTasks = false;
|
|
363
|
+
// stdout carry-over for the control lines below; holds at most the current partial line.
|
|
364
|
+
let controlScanBuffer = '';
|
|
365
|
+
/** Resolves the waiting interrupt this `control_response` line acknowledges, if any. @complexity O(line length). */
|
|
366
|
+
function settleInterruptAck(line) {
|
|
367
|
+
const requestId = controlResponseRequestId({ line: line });
|
|
368
|
+
const settle = requestId === undefined ? undefined : pendingInterruptAcks.get(requestId);
|
|
369
|
+
if (!settle)
|
|
358
370
|
return;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
371
|
+
pendingInterruptAcks.delete(requestId);
|
|
372
|
+
settle(true);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Tracks a `background_tasks_changed` line. The last background task finishing while stdin is
|
|
376
|
+
* held only for them closes it, exactly as the turn end would have: the CLI then answers what it
|
|
377
|
+
* still owes (the tasks' report) and exits. @complexity O(line length).
|
|
378
|
+
*/
|
|
379
|
+
function updateBackgroundTasks(line) {
|
|
380
|
+
const count = backgroundTaskListLength({ line: line });
|
|
381
|
+
if (count === undefined)
|
|
382
|
+
return;
|
|
383
|
+
backgroundTaskCount = count;
|
|
384
|
+
if (count === 0 && heldForBackgroundTasks) {
|
|
385
|
+
heldForBackgroundTasks = false;
|
|
386
|
+
closeStdinOnce();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Reads the stdout lines a mid-run message depends on: interrupt acks and background-task
|
|
391
|
+
* changes. Only agents that take mid-run messages are scanned. A chunk with no newline is only
|
|
392
|
+
* appended, so a long line split across many chunks is split once, not once per chunk.
|
|
393
|
+
* @complexity O(chunk length) amortized.
|
|
394
|
+
*/
|
|
395
|
+
function scanControlLines(text) {
|
|
396
|
+
if (!acceptsMidRunUserMessages({ def: def }))
|
|
397
|
+
return;
|
|
398
|
+
controlScanBuffer += text;
|
|
399
|
+
if (!text.includes('\n'))
|
|
400
|
+
return;
|
|
401
|
+
const lines = controlScanBuffer.split('\n');
|
|
402
|
+
controlScanBuffer = lines.pop() ?? '';
|
|
362
403
|
for (const line of lines) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
continue;
|
|
369
|
-
pendingInterruptAcks.delete(requestId);
|
|
370
|
-
settle(true);
|
|
404
|
+
// Both checked: either word can also appear inside the other kind of line's text.
|
|
405
|
+
if (line.includes('"control_response"'))
|
|
406
|
+
settleInterruptAck(line);
|
|
407
|
+
if (line.includes('"background_tasks_changed"'))
|
|
408
|
+
updateBackgroundTasks(line);
|
|
371
409
|
}
|
|
372
|
-
if (pendingInterruptAcks.size === 0)
|
|
373
|
-
ackScanBuffer = '';
|
|
374
410
|
}
|
|
375
411
|
/**
|
|
376
412
|
* Writes a structured (never string-concatenated — see `ContinuationOptions`'s doc on the
|
|
@@ -392,19 +428,24 @@ export function wireChildLifecycle(ctx) {
|
|
|
392
428
|
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
|
|
393
429
|
}
|
|
394
430
|
/**
|
|
395
|
-
* Whether a `turn_end` leaves stdin open for a live agent that takes mid-run messages
|
|
396
|
-
*
|
|
431
|
+
* Whether a `turn_end` leaves stdin open for a live agent that takes mid-run messages — and why:
|
|
432
|
+
* `'background-tasks'` when only background tasks keep the run going (no turn runs until the next
|
|
433
|
+
* message or task report), `'turn-continues'` for the reasons below, `'close'` otherwise. Open
|
|
434
|
+
* because the run is not over: a `tool_use` stop (the CLI runs the tool itself and carries on, and a
|
|
397
435
|
* message sent while the tool runs must still reach it), any end while an interrupt still waits
|
|
398
436
|
* for its ack (the message is about to be written), and the end of a turn an interrupt aborted
|
|
399
|
-
* (the message written after it starts the next turn)
|
|
400
|
-
*
|
|
437
|
+
* (the message written after it starts the next turn), and any end while background tasks still
|
|
438
|
+
* run (see `backgroundTaskCount`). Every other end closes stdin, so the CLI exits once it has
|
|
439
|
+
* answered everything written before EOF.
|
|
401
440
|
*/
|
|
402
|
-
function
|
|
441
|
+
function stdinHoldAfterTurnEnd(stopReason) {
|
|
403
442
|
if (!acceptsMidRunUserMessages({ def: def }))
|
|
404
|
-
return
|
|
443
|
+
return 'close';
|
|
405
444
|
if (stopReason === 'tool_use' || pendingInterruptAcks.size > 0)
|
|
406
|
-
return
|
|
407
|
-
|
|
445
|
+
return 'turn-continues';
|
|
446
|
+
if (interruptSent && stopReason !== undefined && stopReason.startsWith('aborted'))
|
|
447
|
+
return 'turn-continues';
|
|
448
|
+
return backgroundTaskCount > 0 ? 'background-tasks' : 'close';
|
|
408
449
|
}
|
|
409
450
|
/**
|
|
410
451
|
* Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
|
|
@@ -425,7 +466,9 @@ export function wireChildLifecycle(ctx) {
|
|
|
425
466
|
resolveContinuationTransport({ def: def }) === 'stdin-injection' &&
|
|
426
467
|
continuation.autonomousToolNames.has(toolUse.name);
|
|
427
468
|
if (!shouldInject) {
|
|
428
|
-
|
|
469
|
+
const hold = stdinHoldAfterTurnEnd(stopReason);
|
|
470
|
+
heldForBackgroundTasks = hold === 'background-tasks';
|
|
471
|
+
if (hold === 'close')
|
|
429
472
|
closeStdinOnce();
|
|
430
473
|
return;
|
|
431
474
|
}
|
|
@@ -580,8 +623,9 @@ export function wireChildLifecycle(ctx) {
|
|
|
580
623
|
}
|
|
581
624
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
|
|
582
625
|
// Before the parser: an ack and the aborted turn's end can share a chunk, and the turn end must
|
|
583
|
-
// see that no interrupt is still waiting (see `
|
|
584
|
-
|
|
626
|
+
// see that no interrupt is still waiting (see `stdinHoldAfterTurnEnd`); a background task
|
|
627
|
+
// starting and the turn's end can share one too.
|
|
628
|
+
scanControlLines(text);
|
|
585
629
|
// Non-null: `streamHandler` is only ever null when `streamFormat === 'plain'` (see its
|
|
586
630
|
// construction above), the branch this statement is provably unreachable from.
|
|
587
631
|
streamHandler.feed({ chunk: text });
|
|
@@ -656,7 +700,8 @@ export function wireChildLifecycle(ctx) {
|
|
|
656
700
|
* An ack that never comes (an agent that ignores control requests) only delays the write by
|
|
657
701
|
* {@link INTERRUPT_ACK_TIMEOUT_MS}: the CLI then folds the line in at its next step. Once
|
|
658
702
|
* `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
|
|
659
|
-
* reach — `'not-running'`.
|
|
703
|
+
* reach — `'not-running'`. Between turns, while stdin is held only for background tasks, there is
|
|
704
|
+
* no generation to stop: the line is written at once and starts the next turn.
|
|
660
705
|
*/
|
|
661
706
|
async function sendUserMessage(text) {
|
|
662
707
|
if (!acceptsMidRunUserMessages({ def: def }))
|
|
@@ -664,6 +709,11 @@ export function wireChildLifecycle(ctx) {
|
|
|
664
709
|
const stdin = child.stdin;
|
|
665
710
|
if (stdinClosed || !stdin)
|
|
666
711
|
return 'not-running';
|
|
712
|
+
if (heldForBackgroundTasks) {
|
|
713
|
+
heldForBackgroundTasks = false;
|
|
714
|
+
writeUserMessage(stdin, text);
|
|
715
|
+
return 'delivered';
|
|
716
|
+
}
|
|
667
717
|
const requestId = `jini-interrupt-${randomUUID()}`;
|
|
668
718
|
interruptSent = true;
|
|
669
719
|
// Capture before requesting the interrupt. Its result can precede the acknowledgement, or
|
|
@@ -687,11 +737,15 @@ export function wireChildLifecycle(ctx) {
|
|
|
687
737
|
pendingInterruptAcks.delete(requestId);
|
|
688
738
|
if (!processAlive || stdinClosed)
|
|
689
739
|
return 'not-running';
|
|
740
|
+
writeUserMessage(stdin, text);
|
|
741
|
+
return 'delivered';
|
|
742
|
+
}
|
|
743
|
+
/** Writes a mid-run message as the next user line, journals it, and emits its `user_message` run event. */
|
|
744
|
+
function writeUserMessage(stdin, text) {
|
|
690
745
|
stdin.write(`${JSON.stringify(streamJsonUserMessage({ content: [{ type: 'text', text: text }] }))}\n`, 'utf8');
|
|
691
746
|
if (journal)
|
|
692
747
|
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(text) }));
|
|
693
748
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
|
|
694
|
-
return 'delivered';
|
|
695
749
|
}
|
|
696
750
|
return {
|
|
697
751
|
closeStdinOnce,
|
|
@@ -729,6 +783,18 @@ function controlResponseRequestId({ line }) {
|
|
|
729
783
|
return undefined;
|
|
730
784
|
}
|
|
731
785
|
}
|
|
786
|
+
/** How many tasks a `background_tasks_changed` line lists, or `undefined` for any other line. */
|
|
787
|
+
function backgroundTaskListLength({ line }) {
|
|
788
|
+
try {
|
|
789
|
+
const parsed = JSON.parse(line);
|
|
790
|
+
if (!isRecord(parsed) || parsed.type !== 'system' || parsed.subtype !== 'background_tasks_changed')
|
|
791
|
+
return undefined;
|
|
792
|
+
return Array.isArray(parsed.tasks) ? parsed.tasks.length : undefined;
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
732
798
|
/** The stream-json stdin envelope for one user message — the initial prompt and every mid-run one. */
|
|
733
799
|
function streamJsonUserMessage({ content }) {
|
|
734
800
|
return { type: 'user', message: { role: 'user', content: content } };
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
* this module.
|
|
91
91
|
*/
|
|
92
92
|
export { type SupportedStreamFormat, type AgentExecutorCompatibility, type AgentRuntimeEventTranslation, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, type McpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, type FailureClassificationContext, type ClassifyFailure, type CreateAgentExecutorOptions, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult, } from './contracts.js';
|
|
93
|
-
export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
|
|
93
|
+
export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
|
|
94
94
|
export { extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, applyAgentTranslationSideEffects, } from './event-translation.js';
|
|
95
95
|
export { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, resolveMcpBridgeForRun, writeMcpJsonIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, } from './mcp-bridge.js';
|
|
96
96
|
export { DEFAULT_BUFFERED_STDOUT_MAX_BYTES, runAcpDispatch, runPiRpcDispatch, } from './events.js';
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
* this module.
|
|
91
91
|
*/
|
|
92
92
|
export { AgentExecutorError, } from './contracts.js';
|
|
93
|
-
export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
|
|
93
|
+
export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
|
|
94
94
|
export { extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, applyAgentTranslationSideEffects, } from './event-translation.js';
|
|
95
95
|
export { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, resolveMcpBridgeForRun, writeMcpJsonIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, } from './mcp-bridge.js';
|
|
96
96
|
export { DEFAULT_BUFFERED_STDOUT_MAX_BYTES, runAcpDispatch, runPiRpcDispatch, } from './events.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn as nodeSpawn, type ChildProcess } from 'node:child_process';
|
|
2
|
-
import { getAgentDef, resolveAgentLaunch, prepareAgentLogFile, preparePromptFileForAgent, type AgentLaunchResolution, type PreparedAgentLogFile, type PreparedPromptFile, type RuntimeAgentDef, type RuntimeBuildOptions, type RuntimeContext } from '@jini-ai/agent-runtime';
|
|
2
|
+
import { type AgentRuntimeState, getAgentDef, resolveAgentLaunch, prepareAgentLogFile, preparePromptFileForAgent, type AgentLaunchResolution, type PreparedAgentLogFile, type PreparedPromptFile, type RuntimeAgentDef, type RuntimeBuildOptions, type RuntimeContext } from '@jini-ai/agent-runtime';
|
|
3
3
|
import { createCommandInvocation } from '@jini-ai/platform';
|
|
4
4
|
import { type ImagePromptDelivery } from '../image-prompt-delivery.js';
|
|
5
5
|
import { type SupportedStreamFormat, type ChildDrivenStreamFormat, type AgentExecutorCompatibility, type AgentExecutorRunInput, type AgentExecutor, type McpBridgeDelivery, type CreateAgentExecutorOptions, type FailBeforeSpawn, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult } from './contracts.js';
|
|
@@ -53,6 +53,13 @@ export declare function assessAgentExecutorCompatibility({ def }: {
|
|
|
53
53
|
export declare function isAgentExecutorSupported({ def }: {
|
|
54
54
|
readonly def: RuntimeAgentDef;
|
|
55
55
|
}): boolean;
|
|
56
|
+
/** Snapshot only host configuration used by spawn and config staging; credentials stay per-run.
|
|
57
|
+
* Returns an independent allowlisted record. @complexity O(1), fixed key list. */
|
|
58
|
+
export declare function selectAgentExecutorHostEnv({ env }: {
|
|
59
|
+
env: NodeJS.ProcessEnv;
|
|
60
|
+
}, { runtimeState }?: {
|
|
61
|
+
runtimeState?: AgentRuntimeState;
|
|
62
|
+
}): NodeJS.ProcessEnv;
|
|
56
63
|
interface ResolvedDefAndFormat {
|
|
57
64
|
readonly def: RuntimeAgentDef;
|
|
58
65
|
readonly streamFormat: SupportedStreamFormat;
|
|
@@ -236,6 +243,8 @@ export declare function resolveSystemPromptOverlayDelivery(input: {
|
|
|
236
243
|
readonly overlay: string | null | undefined;
|
|
237
244
|
readonly prompt: string;
|
|
238
245
|
readonly resumeSessionId: string | null | undefined;
|
|
246
|
+
}, { state }?: {
|
|
247
|
+
state?: AgentRuntimeState;
|
|
239
248
|
}): SystemPromptOverlayDelivery;
|
|
240
249
|
/**
|
|
241
250
|
* Phase 9b: calls the def's `buildArgs`, releasing staged resources and failing the run on a throw.
|
|
@@ -370,6 +379,6 @@ export declare function confirmChildSpawned({ input, deps }: {
|
|
|
370
379
|
* @complexity `run()`'s own setup is O(1); steady-state cost is the chosen stream parser's.
|
|
371
380
|
* @overallScore 100/100
|
|
372
381
|
*/
|
|
373
|
-
export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
|
|
382
|
+
export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "runtimeState" | "hostEnv" | "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
|
|
374
383
|
export {};
|
|
375
384
|
//# sourceMappingURL=launch.d.ts.map
|
|
@@ -2,7 +2,7 @@ import { spawn as nodeSpawn, } from 'node:child_process';
|
|
|
2
2
|
import { promises as fsPromises, } from 'node:fs';
|
|
3
3
|
import { tmpdir, } from 'node:os';
|
|
4
4
|
import { join, } from 'node:path';
|
|
5
|
-
import { agentCapabilities, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
|
|
5
|
+
import { agentCapabilities, bindRuntimeAgentDef, defaultRuntimeState, getRuntimeModelDiscoveryDeps, userToolchainBinDirs, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
|
|
6
6
|
import { collectProcessTreePids, createCommandInvocation, listProcessSnapshots, stopProcesses, } from '@jini-ai/platform';
|
|
7
7
|
import { applyImagePromptDelivery, } from '../image-prompt-delivery.js';
|
|
8
8
|
import { SUPPORTED_STREAM_FORMATS, AgentExecutorError, } from './contracts.js';
|
|
@@ -131,6 +131,22 @@ function buildAgentEnv(hostEnv, credentialEnv) {
|
|
|
131
131
|
}
|
|
132
132
|
return result;
|
|
133
133
|
}
|
|
134
|
+
/** Snapshot only host configuration used by spawn and config staging; credentials stay per-run.
|
|
135
|
+
* Returns an independent allowlisted record. @complexity O(1), fixed key list. */
|
|
136
|
+
export function selectAgentExecutorHostEnv({ env }, { runtimeState } = {}) {
|
|
137
|
+
const selected = buildAgentEnv(env, undefined);
|
|
138
|
+
for (const key of ['CODEX_HOME', 'CLAUDE_CONFIG_DIR', 'VELA_OPENCODE_BIN', 'AGENT_RUNTIME_HOME', 'AGENT_RUNTIME_RESOURCE_ROOT', 'VP_HOME', 'NPM_CONFIG_PREFIX', 'npm_config_prefix', 'APPDATA', 'LOCALAPPDATA', 'MISE_DATA_DIR', 'FNM_DIR']) {
|
|
139
|
+
if (typeof env[key] === 'string')
|
|
140
|
+
selected[key] = env[key];
|
|
141
|
+
}
|
|
142
|
+
// Host-configured resolution names are explicit configuration, never a scan of the env bag.
|
|
143
|
+
const state = runtimeState ?? defaultRuntimeState;
|
|
144
|
+
for (const key of Object.values(state.executable.envNames)) {
|
|
145
|
+
if (typeof env[key] === 'string')
|
|
146
|
+
selected[key] = env[key];
|
|
147
|
+
}
|
|
148
|
+
return selected;
|
|
149
|
+
}
|
|
134
150
|
/**
|
|
135
151
|
* Resolves once `child` emits `'spawn'`, or rejects on `'error'`. Replicates
|
|
136
152
|
* `@jini-ai/platform`'s own internal (non-exported) `waitForChildSpawn` race
|
|
@@ -156,11 +172,17 @@ function waitForSpawnOrError(child) {
|
|
|
156
172
|
* hiding which options belong together. Pure.
|
|
157
173
|
*/
|
|
158
174
|
function resolveAgentRuntimeDeps(options) {
|
|
175
|
+
const runtimeOptions = options.runtimeState ? { state: options.runtimeState } : {};
|
|
176
|
+
const envOptions = options.hostEnv ? { env: options.hostEnv } : {};
|
|
177
|
+
const lookupDef = options.getAgentDef ?? getAgentDef;
|
|
159
178
|
return {
|
|
160
|
-
getAgentDef:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
getAgentDef: (input) => {
|
|
180
|
+
const def = lookupDef(input);
|
|
181
|
+
return def && options.runtimeState ? bindRuntimeAgentDef({ def, state: options.runtimeState }) : def;
|
|
182
|
+
},
|
|
183
|
+
resolveAgentLaunch: options.resolveAgentLaunch ?? ((input, optional = {}) => resolveAgentLaunch(input, { ...optional, ...runtimeOptions, ...envOptions })),
|
|
184
|
+
ensureAgentCapabilities: options.ensureAgentCapabilities ?? ((input) => ensureAgentCapabilities(input, runtimeOptions)),
|
|
185
|
+
applyAgentLaunchEnv: options.applyAgentLaunchEnv ?? ((input, optional = {}) => applyAgentLaunchEnv(input, { appendPathDirs: userToolchainBinDirs({}, { ...runtimeOptions, ...envOptions }), ...optional })),
|
|
164
186
|
attachAcpSession: options.attachAcpSession ?? attachAcpSession,
|
|
165
187
|
attachPiRpcSession: options.attachPiRpcSession ?? attachPiRpcSession,
|
|
166
188
|
preparePromptFileForAgent: options.preparePromptFileForAgent ?? preparePromptFileForAgent,
|
|
@@ -397,7 +419,7 @@ export function buildAgentBuildArgsOptions({ input, systemPromptOverlay }) {
|
|
|
397
419
|
* @complexity O(n) in the overlay/prompt lengths — string concatenation only, no I/O.
|
|
398
420
|
* @overallScore 100/100
|
|
399
421
|
*/
|
|
400
|
-
export function resolveSystemPromptOverlayDelivery(input) {
|
|
422
|
+
export function resolveSystemPromptOverlayDelivery(input, { state } = {}) {
|
|
401
423
|
const { defId, systemPromptDelivery, resumesSessionViaCli, resumesSessionViaAcpLoad, overlay, prompt, resumeSessionId } = input;
|
|
402
424
|
if (typeof overlay !== 'string' || overlay.length === 0) {
|
|
403
425
|
return { promptPrefix: '', prompt, extraArgs: [], envOverrides: {} };
|
|
@@ -410,7 +432,7 @@ export function resolveSystemPromptOverlayDelivery(input) {
|
|
|
410
432
|
// `--help` probe ran and did not find the flag) withholds it. `capabilityKey === undefined`
|
|
411
433
|
// (e.g. `pi`'s existing `--append-system-prompt`, trusted unconditionally) always passes, same
|
|
412
434
|
// as an absent key.
|
|
413
|
-
const capabilityOk = capabilityKey === undefined || agentCapabilities.get(defId)?.[capabilityKey] !== false;
|
|
435
|
+
const capabilityOk = capabilityKey === undefined || (state?.capabilities ?? agentCapabilities).get(defId)?.[capabilityKey] !== false;
|
|
414
436
|
return { promptPrefix: '', prompt, extraArgs: capabilityOk ? [systemPromptDelivery.flag, overlay] : [], envOverrides: {} };
|
|
415
437
|
}
|
|
416
438
|
if (systemPromptDelivery?.strategy === 'env-var') {
|
|
@@ -607,7 +629,8 @@ export async function confirmChildSpawned({ input, deps }) {
|
|
|
607
629
|
export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
608
630
|
const options = { ...requiredArgs, ...optionalArgs };
|
|
609
631
|
const lifecycle = options.lifecycle;
|
|
610
|
-
const {
|
|
632
|
+
const hostEnv = selectAgentExecutorHostEnv({ env: options.hostEnv ?? process.env }, options.runtimeState ? { runtimeState: options.runtimeState } : {});
|
|
633
|
+
const { getAgentDef: getAgentDefFn, resolveAgentLaunch: resolveAgentLaunchFn, ensureAgentCapabilities: ensureAgentCapabilitiesFn, applyAgentLaunchEnv: applyAgentLaunchEnvFn, attachAcpSession: attachAcpSessionFn, attachPiRpcSession: attachPiRpcSessionFn, preparePromptFileForAgent: preparePromptFileForAgentFn, prepareAgentLogFile: prepareAgentLogFileFn, } = resolveAgentRuntimeDeps({ ...options, hostEnv });
|
|
611
634
|
const { createCommandInvocation: createCommandInvocationFn, spawn: spawnFn, listProcessSnapshots: listProcessSnapshotsFn, collectProcessTreePids: collectProcessTreePidsFn, stopProcesses: stopProcessesFn, } = resolveProcessDeps(options);
|
|
612
635
|
const { onCleanupFailure: onCleanupFailureFn, bufferedStdoutMaxBytes } = resolveMiscExecutorDeps(options);
|
|
613
636
|
const journal = options.journal;
|
|
@@ -711,18 +734,19 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
711
734
|
overlay: systemPromptOverlay,
|
|
712
735
|
prompt: imageDelivery.prompt,
|
|
713
736
|
resumeSessionId: preStagingRuntimeContext?.resumeSessionId,
|
|
714
|
-
});
|
|
737
|
+
}, options.runtimeState ? { state: options.runtimeState } : {});
|
|
715
738
|
// The def's own declared env (e.g. claude's `ENABLE_TOOL_SEARCH=false`) is a default the run's
|
|
716
739
|
// env can override. Detection already spawns with it (`detection.ts`); without this the run
|
|
717
740
|
// spawn silently dropped it. Declared by the def itself, so it never widens SEC-001's allowlist.
|
|
718
|
-
const resolvedEnv = { ...(def.env ?? {}), ...resolveRunEnv({ input: input, hostEnv
|
|
741
|
+
const resolvedEnv = { ...(def.env ?? {}), ...resolveRunEnv({ input: input, hostEnv }) };
|
|
719
742
|
const launch = await resolveLaunch({ input: { runId: input.runId, def, resolvedEnv }, deps: { resolveAgentLaunch: resolveAgentLaunchFn, failBeforeSpawn } });
|
|
720
743
|
const spawnEnv = applyAgentLaunchEnvFn({ env: { ...resolvedEnv }, launch });
|
|
721
744
|
// Resolve from the launch environment before staging or sending any prompt. Pin the same
|
|
722
745
|
// concrete ID that the picker displays; an opaque native route cannot silently start a run.
|
|
723
746
|
let modelCatalog;
|
|
724
747
|
try {
|
|
725
|
-
const
|
|
748
|
+
const selectModel = options.resolveModelForLaunch ?? ((input) => resolveModelForLaunch(input, options.runtimeState ? { deps: getRuntimeModelDiscoveryDeps({ state: options.runtimeState }) } : {}));
|
|
749
|
+
const selection = await selectModel({ def, context: {
|
|
726
750
|
executable: launch.launchPath, cwd: input.cwd, env: spawnEnv,
|
|
727
751
|
...(input.model && input.model !== 'default' ? { model: input.model } : {}),
|
|
728
752
|
...(input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {}),
|
|
@@ -868,7 +892,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
868
892
|
// `codex.ts`'s `buildArgs` needs no argv change for this (CODEX_HOME is an env var, not a flag),
|
|
869
893
|
// so — unlike the `.mcp.json` staging above — this can run after `buildArgs` with no ordering
|
|
870
894
|
// constraint of its own; it is placed here only to keep the two staging steps adjacent.
|
|
871
|
-
preparedCodexHome = await prepareCodexHomeIfNeeded({ input: { runId: input.runId, def, mcpBridge }, deps: { mcpJsonInjection, hostEnv
|
|
895
|
+
preparedCodexHome = await prepareCodexHomeIfNeeded({ input: { runId: input.runId, def, mcpBridge }, deps: { mcpJsonInjection, hostEnv, releaseStagedResources, failBeforeSpawn } });
|
|
872
896
|
// Finding 1 of SEC-assistant-env-isolation-2026-09-07's one effect — stage this run's scratch
|
|
873
897
|
// `CLAUDE_CONFIG_DIR` directory. Unconditional for a `claude`-id run (unlike CODEX_HOME above,
|
|
874
898
|
// this does not depend on `mcpJsonInjection` being configured at all — see
|
|
@@ -878,7 +902,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
878
902
|
preparedClaudeConfigDir = await prepareClaudeConfigDirIfNeeded({ input: { runId: input.runId, def }, deps: {
|
|
879
903
|
enabled: claudeConfigDirIsolationEnabled,
|
|
880
904
|
claudeConfigDirIsolation,
|
|
881
|
-
hostEnv
|
|
905
|
+
hostEnv,
|
|
882
906
|
releaseStagedResources,
|
|
883
907
|
failBeforeSpawn,
|
|
884
908
|
} });
|
package/dist/agent-executor.d.ts
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* wrappers or a second implementation would let tested and shipped behavior drift.
|
|
7
7
|
* See agent-executor/index.ts for the driver contracts and invariants.
|
|
8
8
|
*/
|
|
9
|
-
export { type SupportedStreamFormat, isSupportedStreamFormat, type AgentExecutorCompatibility, assessAgentExecutorCompatibility, isAgentExecutorSupported, type AgentRuntimeEventTranslation, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, type McpBridgeDelivery, buildMcpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, resolveSourceClaudeConfigDir, type FailureClassificationContext, type ClassifyFailure, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, type CreateAgentExecutorOptions, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, type SystemPromptOverlayDelivery, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, type PreparedSystemPromptOverlayFile, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, type SpawnAgentChildProcessResult, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
|
|
9
|
+
export { type SupportedStreamFormat, isSupportedStreamFormat, type AgentExecutorCompatibility, assessAgentExecutorCompatibility, isAgentExecutorSupported, type AgentRuntimeEventTranslation, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, type McpBridgeDelivery, buildMcpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, resolveSourceClaudeConfigDir, type FailureClassificationContext, type ClassifyFailure, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, type CreateAgentExecutorOptions, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, type SystemPromptOverlayDelivery, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, type PreparedSystemPromptOverlayFile, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, type SpawnAgentChildProcessResult, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
|
|
10
10
|
//# sourceMappingURL=agent-executor.d.ts.map
|
package/dist/agent-executor.js
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* wrappers or a second implementation would let tested and shipped behavior drift.
|
|
7
7
|
* See agent-executor/index.ts for the driver contracts and invariants.
|
|
8
8
|
*/
|
|
9
|
-
export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, AgentExecutorError, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
|
|
9
|
+
export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, AgentExecutorError, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
|
|
10
10
|
//# sourceMappingURL=agent-executor.js.map
|
package/dist/http/index.d.ts
CHANGED
|
@@ -40,8 +40,8 @@ export type { HealthHttpDeps, HealthReadinessResult, LivenessResponse, Readiness
|
|
|
40
40
|
export { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyRoute, registerHealthRoutes, versionInfoRoute, } from './health.js';
|
|
41
41
|
export type { AuthCredentials, AuthProvider, AuthSession, AuthUser, Charge, ChargeInput, ChargeStatus, ConnectorsAuthSessionResponse, ConnectorsAuthUserResponse, ConnectorsAuthVerifyResponse, ConnectorsChargeResponse, ConnectorsDbQueryResponse, ConnectorsDbRecordResponse, ConnectorsHttpDeps, ConnectorsInternalErrorContext, ConnectorsOkResponse, ConnectorsStorageGetResponse, ConnectorsStorageListResponse, ConnectorsStorageMetaResponse, DbProvider, DbQuery, DbRecord, PaymentsProvider, RealtimeProvider, StorageObjectMeta, StorageProvider, StoragePutOptions, } from './connectors.js';
|
|
42
42
|
export { connectorsAuthSessionRoute, connectorsAuthSignInRoute, connectorsAuthSignOutRoute, connectorsAuthSignUpRoute, connectorsDbDeleteRoute, connectorsDbGetRoute, connectorsDbInsertRoute, connectorsDbQueryRoute, connectorsDbUpdateRoute, connectorsPaymentsChargeRoute, connectorsPaymentsGetRoute, connectorsPaymentsRefundRoute, connectorsRealtimePublishRoute, connectorsStorageDeleteRoute, connectorsStorageGetRoute, connectorsStorageListRoute, connectorsStoragePutRoute, registerConnectorsRoutes, } from './connectors.js';
|
|
43
|
-
export type { ResearchHttpDeps, ResearchInternalErrorContext, ResearchProviderCredentials, ResearchSearchResponse, ResearchSource, } from './research.js';
|
|
44
|
-
export { registerResearchRoutes, researchSearchRoute } from './research.js';
|
|
43
|
+
export type { ResearchProvider, ResearchSearchRequest, ResearchHttpDeps, ResearchInternalErrorContext, ResearchProviderCredentials, ResearchSearchResponse, ResearchSource, } from './research.js';
|
|
44
|
+
export { createResearchSearchRoute, registerResearchRoutes, researchSearchRoute } from './research.js';
|
|
45
45
|
export type { MediaGenerateResponse, MediaHttpDeps, MediaInternalErrorContext, MediaTaskDeleteResponse, MediaTaskListResponse, MediaTaskResponse, } from './media.js';
|
|
46
46
|
export { mediaGenerateRoute, mediaTaskDeleteRoute, mediaTaskGetRoute, mediaTaskListRoute, registerMediaRoutes, } from './media.js';
|
|
47
47
|
export type { AttachmentClaim, AttachmentRejectionReason, AttachmentsHttpDeps, AttachmentsInternalErrorContext, AttachmentStore, AttachmentUploadResponse, CreateDiskAttachmentStoreOptions, ObservedAttachmentIdentity, PendingAttachmentSummary, RecordedAttachmentIdentity, StoredAttachment, } from './attachments.js';
|
|
@@ -49,4 +49,5 @@ export { ATTACHMENTS_ROUTE_PATH, AttachmentRejectedError, createDiskAttachmentSt
|
|
|
49
49
|
export type { XaiAuthStatusResponse, XaiHttpDeps, XaiInternalErrorContext, XaiOauthStartResponse, XaiOkResponse, XaiSearchResponse, } from './xai.js';
|
|
50
50
|
export { registerXaiRoutes, xaiAuthStatusRoute, xaiOauthCancelRoute, xaiOauthCompleteRoute, xaiOauthDisconnectRoute, xaiOauthStartRoute, xaiSearchRoute, } from './xai.js';
|
|
51
51
|
export * from './run-credentials.js';
|
|
52
|
+
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
52
53
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.js
CHANGED
|
@@ -19,9 +19,10 @@ export { registerTerminalEventStream, registerTerminalRoutes, terminalCreateRout
|
|
|
19
19
|
export { registerModelProxyRoutes } from './model-proxy.js';
|
|
20
20
|
export { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyRoute, registerHealthRoutes, versionInfoRoute, } from './health.js';
|
|
21
21
|
export { connectorsAuthSessionRoute, connectorsAuthSignInRoute, connectorsAuthSignOutRoute, connectorsAuthSignUpRoute, connectorsDbDeleteRoute, connectorsDbGetRoute, connectorsDbInsertRoute, connectorsDbQueryRoute, connectorsDbUpdateRoute, connectorsPaymentsChargeRoute, connectorsPaymentsGetRoute, connectorsPaymentsRefundRoute, connectorsRealtimePublishRoute, connectorsStorageDeleteRoute, connectorsStorageGetRoute, connectorsStorageListRoute, connectorsStoragePutRoute, registerConnectorsRoutes, } from './connectors.js';
|
|
22
|
-
export { registerResearchRoutes, researchSearchRoute } from './research.js';
|
|
22
|
+
export { createResearchSearchRoute, registerResearchRoutes, researchSearchRoute } from './research.js';
|
|
23
23
|
export { mediaGenerateRoute, mediaTaskDeleteRoute, mediaTaskGetRoute, mediaTaskListRoute, registerMediaRoutes, } from './media.js';
|
|
24
24
|
export { ATTACHMENTS_ROUTE_PATH, AttachmentRejectedError, createDiskAttachmentStore, detectAttachmentKind, handleAttachmentCleanup, handleAttachmentUpload, isUnchangedAttachment, registerAttachmentRoutes, sanitizeAttachmentName, writeBoundedAttachmentBody, } from './attachments.js';
|
|
25
25
|
export { registerXaiRoutes, xaiAuthStatusRoute, xaiOauthCancelRoute, xaiOauthCompleteRoute, xaiOauthDisconnectRoute, xaiOauthStartRoute, xaiSearchRoute, } from './xai.js';
|
|
26
26
|
export * from './run-credentials.js';
|
|
27
|
+
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
27
28
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ResearchProvider } from './research.js';
|
|
2
|
+
/** Creates today's transport with a host-owned fetch port. */
|
|
3
|
+
export declare function createTavilyResearchProvider(_required: Record<string, never>, options?: {
|
|
4
|
+
fetch?: typeof globalThis.fetch;
|
|
5
|
+
}): ResearchProvider;
|
|
6
|
+
//# sourceMappingURL=research-tavily.adapter.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Tavily default adapter. Request limits, timeout and secret-redaction policy remain here. */
|
|
2
|
+
import { validateBaseUrl } from '@jini-ai/agent-runtime';
|
|
3
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
4
|
+
const DEFAULT_TAVILY_BASE_URL = 'https://api.tavily.com';
|
|
5
|
+
/** Tavily's own documented cap on `max_results` (see OD's `tavily.ts#TAVILY_MAX_RESULTS_LIMIT`). */
|
|
6
|
+
const TAVILY_MAX_RESULTS_LIMIT = 20;
|
|
7
|
+
const DEFAULT_MAX_SOURCES = 5;
|
|
8
|
+
/** Matches OD's own `tavily.ts` timeout — see research.ts's module doc. */
|
|
9
|
+
const DEFAULT_TAVILY_TIMEOUT_MS = 30_000;
|
|
10
|
+
/**
|
|
11
|
+
* Calls Tavily's real `POST /search` endpoint directly — see research.ts's module doc for the port's exact
|
|
12
|
+
* scope/provenance. Throws (never a `Result`) on any failure; the route handler in research.ts is the one
|
|
13
|
+
* place that converts a thrown error into the package's SEC-005 response shape.
|
|
14
|
+
*
|
|
15
|
+
* `apiKey` is a separate, non-optional parameter rather than being read off `credentials`, so the
|
|
16
|
+
* "credential is present" invariant is carried by the type system instead of by a runtime guard.
|
|
17
|
+
* This function used to take only `credentials` and re-check `credentials.apiKey` itself, but the
|
|
18
|
+
* route caller in research.ts already returns `NOT_CONFIGURED` before it ever gets here — the check was
|
|
19
|
+
* unreachable by construction, so it is now expressed as a required argument instead of dead code.
|
|
20
|
+
* `credentials` is still passed for its optional transport config (`baseUrl`).
|
|
21
|
+
*/
|
|
22
|
+
async function tavilySearch({ apiKey, credentials, request }, { fetch: fetchImpl = globalThis.fetch } = {}) {
|
|
23
|
+
const baseUrl = credentials.baseUrl ?? DEFAULT_TAVILY_BASE_URL;
|
|
24
|
+
const baseUrlCheck = validateBaseUrl({ baseUrl });
|
|
25
|
+
if (baseUrlCheck.error) {
|
|
26
|
+
throw new Error(baseUrlCheck.error);
|
|
27
|
+
}
|
|
28
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
29
|
+
// Floor before clamping, matching OD's real `clampMaxSources` — see research.ts's module doc. Without the
|
|
30
|
+
// floor, a fractional `maxSources` (e.g. `2.7`, or `0.5` before the `Math.max(1, ...)` floor
|
|
31
|
+
// below) would forward a non-integer `max_results` straight to Tavily's real API.
|
|
32
|
+
const maxResults = Math.max(1, Math.min(Math.floor(request.maxSources ?? DEFAULT_MAX_SOURCES), TAVILY_MAX_RESULTS_LIMIT));
|
|
33
|
+
const body = {
|
|
34
|
+
query: request.query,
|
|
35
|
+
search_depth: 'basic',
|
|
36
|
+
max_results: maxResults,
|
|
37
|
+
include_answer: true,
|
|
38
|
+
include_raw_content: false,
|
|
39
|
+
};
|
|
40
|
+
// The timeout must stay armed for the full operation, not just until `fetch` resolves —
|
|
41
|
+
// `fetch` resolves as soon as response headers arrive, before the body is read. A round-2
|
|
42
|
+
// external audit caught (and locally reproduced) an earlier version of this fix that cleared
|
|
43
|
+
// the timer right after `fetch` resolved, leaving `response.text()`/`response.json()` below
|
|
44
|
+
// completely unbounded against a server that sends headers promptly and then stalls the body.
|
|
45
|
+
//
|
|
46
|
+
// `TavilyHttpError` marks an error already redacted and formatted by the `!response.ok` branch
|
|
47
|
+
// below, so the outer catch can rethrow it verbatim instead of wrapping it a second time (which
|
|
48
|
+
// would otherwise produce a "Tavily request failed: Tavily 429: ..." double-prefixed message).
|
|
49
|
+
class TavilyHttpError extends Error {
|
|
50
|
+
}
|
|
51
|
+
const timeoutController = new AbortController();
|
|
52
|
+
const timeoutHandle = setTimeout(() => timeoutController.abort(), DEFAULT_TAVILY_TIMEOUT_MS);
|
|
53
|
+
// Assigned in the `try` and returned *after* the `finally`, rather than returned from inside the
|
|
54
|
+
// `try`. Both forms behave identically, but a return-through-`finally` compiles to three copies of
|
|
55
|
+
// the `finally` body (normal completion, return, throw) and one of those is unreachable here — the
|
|
56
|
+
// `try` cannot fall off its end and the `catch` always throws. Falling through to a single return
|
|
57
|
+
// keeps the cleanup path honestly measurable instead of permanently one-third uncovered.
|
|
58
|
+
let output;
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetchImpl(`${base}/search`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
signal: timeoutController.signal,
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const text = await response.text().catch(() => '');
|
|
68
|
+
throw new TavilyHttpError(redactSecrets({ input: `Tavily ${response.status}: ${text.slice(0, 200) || 'no body'}` }, { exactSecrets: [apiKey] }));
|
|
69
|
+
}
|
|
70
|
+
const json = (await response.json());
|
|
71
|
+
const answer = typeof json.answer === 'string' ? json.answer : '';
|
|
72
|
+
const rawResults = Array.isArray(json.results) ? json.results : [];
|
|
73
|
+
const sources = [];
|
|
74
|
+
for (const raw of rawResults) {
|
|
75
|
+
const url = typeof raw.url === 'string' ? raw.url : '';
|
|
76
|
+
if (!url)
|
|
77
|
+
continue;
|
|
78
|
+
const publishedAt = typeof raw.published_date === 'string' && raw.published_date.trim() ? raw.published_date.trim() : undefined;
|
|
79
|
+
sources.push({
|
|
80
|
+
title: typeof raw.title === 'string' && raw.title.trim() ? raw.title.trim() : url,
|
|
81
|
+
url,
|
|
82
|
+
snippet: typeof raw.content === 'string' ? raw.content.trim().slice(0, 800) : '',
|
|
83
|
+
provider: 'tavily',
|
|
84
|
+
...(publishedAt ? { publishedAt } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
output = { answer, sources };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!timeoutController.signal.aborted && error instanceof TavilyHttpError) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
throw new Error(redactSecrets({
|
|
95
|
+
input: timeoutController.signal.aborted
|
|
96
|
+
? `Tavily request timed out after ${DEFAULT_TAVILY_TIMEOUT_MS}ms`
|
|
97
|
+
: `Tavily request failed: ${message}`,
|
|
98
|
+
}, { exactSecrets: [apiKey] }));
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
clearTimeout(timeoutHandle);
|
|
102
|
+
}
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
/** Creates today's transport with a host-owned fetch port. */
|
|
106
|
+
export function createTavilyResearchProvider(_required, options = {}) {
|
|
107
|
+
return { id: 'tavily', search: (input) => tavilySearch(input, options) };
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=research-tavily.adapter.js.map
|
package/dist/http/research.d.ts
CHANGED
|
@@ -25,7 +25,22 @@ export interface ResearchProviderCredentials {
|
|
|
25
25
|
readonly baseUrl?: string;
|
|
26
26
|
readonly model?: string;
|
|
27
27
|
}
|
|
28
|
+
/** Research transport contract: the route owns validation/HTTP errors; adapters own vendor I/O. */
|
|
29
|
+
export interface ResearchProvider {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
search(required: {
|
|
32
|
+
apiKey: string;
|
|
33
|
+
credentials: ResearchProviderCredentials;
|
|
34
|
+
request: ResearchSearchRequest;
|
|
35
|
+
}, optional: Record<string, never>): Promise<{
|
|
36
|
+
readonly answer: string;
|
|
37
|
+
readonly sources: readonly ResearchSource[];
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
28
40
|
export interface ResearchHttpDeps {
|
|
41
|
+
readonly provider?: ResearchProvider;
|
|
42
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
43
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
29
44
|
/** Resolves per-provider credentials. Defaults to `TAVILY_API_KEY` for the built-in Tavily transport. */
|
|
30
45
|
readonly resolveCredentials?: ({ providerId }: {
|
|
31
46
|
providerId: string;
|
|
@@ -33,10 +48,14 @@ export interface ResearchHttpDeps {
|
|
|
33
48
|
/** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` response (SEC-005). Defaults to `console.error`. */
|
|
34
49
|
readonly onInternalError?: (context: ResearchInternalErrorContext) => void;
|
|
35
50
|
}
|
|
36
|
-
interface ResearchSearchRequest {
|
|
51
|
+
export interface ResearchSearchRequest {
|
|
37
52
|
readonly query: string;
|
|
38
53
|
readonly maxSources?: number;
|
|
39
54
|
}
|
|
55
|
+
/** Builds one route over a host-selected provider, preserving the default Tavily validation contract. */
|
|
56
|
+
export declare function createResearchSearchRoute({ provider }: {
|
|
57
|
+
provider?: ResearchProvider;
|
|
58
|
+
}, _optional?: Record<string, never>): import("@jini-ai/http-kit").JsonRouteSpec<ResearchSearchRequest, ResearchSearchResponse, ResearchHttpDeps, undefined>;
|
|
40
59
|
export declare const researchSearchRoute: import("@jini-ai/http-kit").JsonRouteSpec<ResearchSearchRequest, ResearchSearchResponse, ResearchHttpDeps, undefined>;
|
|
41
60
|
/** Mounts `POST /api/research/search` on `app`. A pack's `http(app, services)` calls this directly. */
|
|
42
61
|
export declare function registerResearchRoutes({ app, deps, adapter }: {
|
|
@@ -44,5 +63,4 @@ export declare function registerResearchRoutes({ app, deps, adapter }: {
|
|
|
44
63
|
readonly deps: ResearchHttpDeps;
|
|
45
64
|
readonly adapter: AdapterContext;
|
|
46
65
|
}, _optional?: Record<string, never>): void;
|
|
47
|
-
export {};
|
|
48
66
|
//# sourceMappingURL=research.d.ts.map
|
package/dist/http/research.js
CHANGED
|
@@ -96,17 +96,11 @@ import { redactSecrets } from '@jini-ai/core';
|
|
|
96
96
|
* generic-message substitution.
|
|
97
97
|
*/
|
|
98
98
|
import { randomUUID } from 'node:crypto';
|
|
99
|
-
import {
|
|
99
|
+
import { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
100
100
|
import { createApiError } from '@jini-ai/protocol';
|
|
101
101
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
102
102
|
import { validationError } from '@jini-ai/http-kit';
|
|
103
103
|
import { err, ok } from '@jini-ai/http-kit';
|
|
104
|
-
const DEFAULT_TAVILY_BASE_URL = 'https://api.tavily.com';
|
|
105
|
-
/** Tavily's own documented cap on `max_results` (see OD's `tavily.ts#TAVILY_MAX_RESULTS_LIMIT`). */
|
|
106
|
-
const TAVILY_MAX_RESULTS_LIMIT = 20;
|
|
107
|
-
const DEFAULT_MAX_SOURCES = 5;
|
|
108
|
-
/** Matches OD's own `tavily.ts` timeout — see module doc. */
|
|
109
|
-
const DEFAULT_TAVILY_TIMEOUT_MS = 30_000;
|
|
110
104
|
/** Matches OD's real `research/index.ts#searchResearch` query cap — see module doc. */
|
|
111
105
|
const MAX_QUERY_LENGTH = 1000;
|
|
112
106
|
function defaultInternalErrorSink(context) {
|
|
@@ -123,8 +117,8 @@ function defaultInternalErrorSink(context) {
|
|
|
123
117
|
* other provider name at parse time. A host that supports a second provider injects its own
|
|
124
118
|
* `resolveCredentials` rather than reaching this function with a different id.
|
|
125
119
|
*/
|
|
126
|
-
async function defaultResolveCredentials() {
|
|
127
|
-
const apiKey =
|
|
120
|
+
async function defaultResolveCredentials({ env }) {
|
|
121
|
+
const apiKey = env.TAVILY_API_KEY?.trim();
|
|
128
122
|
return apiKey ? { apiKey } : {};
|
|
129
123
|
}
|
|
130
124
|
function isRecord(value) {
|
|
@@ -138,17 +132,17 @@ function nonEmptyString(value) {
|
|
|
138
132
|
* doc). OD only inspects `providers[0]` and treats a non-array `providers` value as absent rather
|
|
139
133
|
* than rejecting it; this mirrors both.
|
|
140
134
|
*/
|
|
141
|
-
function validateRequestedProvider(rawProviders) {
|
|
135
|
+
function validateRequestedProvider(rawProviders, providerId) {
|
|
142
136
|
if (!Array.isArray(rawProviders))
|
|
143
137
|
return ok({ value: null });
|
|
144
138
|
const filtered = rawProviders.filter((p) => typeof p === 'string' && p.length > 0);
|
|
145
139
|
const requested = filtered[0];
|
|
146
|
-
if (requested !== undefined && requested !==
|
|
147
|
-
return err({ error: validationError({ message: `provider "${requested}" is not supported — only "
|
|
140
|
+
if (requested !== undefined && requested !== providerId) {
|
|
141
|
+
return err({ error: validationError({ message: `provider "${requested}" is not supported — only "${providerId}" is available` }) });
|
|
148
142
|
}
|
|
149
143
|
return ok({ value: null });
|
|
150
144
|
}
|
|
151
|
-
function parseResearchSearch(input) {
|
|
145
|
+
function parseResearchSearch(input, providerId = 'tavily') {
|
|
152
146
|
if (!isRecord(input.body))
|
|
153
147
|
return err({ error: validationError({ message: 'body must be a JSON object' }) });
|
|
154
148
|
const rawQuery = nonEmptyString(input.body.query);
|
|
@@ -159,106 +153,11 @@ function parseResearchSearch(input) {
|
|
|
159
153
|
if (maxSources !== undefined && (typeof maxSources !== 'number' || !Number.isFinite(maxSources) || maxSources <= 0)) {
|
|
160
154
|
return err({ error: validationError({ message: 'maxSources must be a positive number when provided' }) });
|
|
161
155
|
}
|
|
162
|
-
const providerCheck = validateRequestedProvider(input.body.providers);
|
|
156
|
+
const providerCheck = validateRequestedProvider(input.body.providers, providerId);
|
|
163
157
|
if (!providerCheck.ok)
|
|
164
158
|
return providerCheck;
|
|
165
159
|
return ok({ value: { query, ...(maxSources !== undefined ? { maxSources: maxSources } : {}) } });
|
|
166
160
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Calls Tavily's real `POST /search` endpoint directly — see module doc for the port's exact
|
|
169
|
-
* scope/provenance. Throws (never a `Result`) on any failure; the route handler below is the one
|
|
170
|
-
* place that converts a thrown error into the package's SEC-005 response shape.
|
|
171
|
-
*
|
|
172
|
-
* `apiKey` is a separate, non-optional parameter rather than being read off `credentials`, so the
|
|
173
|
-
* "credential is present" invariant is carried by the type system instead of by a runtime guard.
|
|
174
|
-
* This function used to take only `credentials` and re-check `credentials.apiKey` itself, but the
|
|
175
|
-
* sole caller below already returns `NOT_CONFIGURED` before it ever gets here — the check was
|
|
176
|
-
* unreachable by construction, so it is now expressed as a required argument instead of dead code.
|
|
177
|
-
* `credentials` is still passed for its optional transport config (`baseUrl`).
|
|
178
|
-
*/
|
|
179
|
-
async function tavilySearch(apiKey, credentials, request) {
|
|
180
|
-
const baseUrl = credentials.baseUrl ?? DEFAULT_TAVILY_BASE_URL;
|
|
181
|
-
const baseUrlCheck = validateBaseUrl({ baseUrl });
|
|
182
|
-
if (baseUrlCheck.error) {
|
|
183
|
-
throw new Error(baseUrlCheck.error);
|
|
184
|
-
}
|
|
185
|
-
const base = baseUrl.replace(/\/+$/, '');
|
|
186
|
-
// Floor before clamping, matching OD's real `clampMaxSources` — see module doc. Without the
|
|
187
|
-
// floor, a fractional `maxSources` (e.g. `2.7`, or `0.5` before the `Math.max(1, ...)` floor
|
|
188
|
-
// below) would forward a non-integer `max_results` straight to Tavily's real API.
|
|
189
|
-
const maxResults = Math.max(1, Math.min(Math.floor(request.maxSources ?? DEFAULT_MAX_SOURCES), TAVILY_MAX_RESULTS_LIMIT));
|
|
190
|
-
const body = {
|
|
191
|
-
query: request.query,
|
|
192
|
-
search_depth: 'basic',
|
|
193
|
-
max_results: maxResults,
|
|
194
|
-
include_answer: true,
|
|
195
|
-
include_raw_content: false,
|
|
196
|
-
};
|
|
197
|
-
// The timeout must stay armed for the full operation, not just until `fetch` resolves —
|
|
198
|
-
// `fetch` resolves as soon as response headers arrive, before the body is read. A round-2
|
|
199
|
-
// external audit caught (and locally reproduced) an earlier version of this fix that cleared
|
|
200
|
-
// the timer right after `fetch` resolved, leaving `response.text()`/`response.json()` below
|
|
201
|
-
// completely unbounded against a server that sends headers promptly and then stalls the body.
|
|
202
|
-
//
|
|
203
|
-
// `TavilyHttpError` marks an error already redacted and formatted by the `!response.ok` branch
|
|
204
|
-
// below, so the outer catch can rethrow it verbatim instead of wrapping it a second time (which
|
|
205
|
-
// would otherwise produce a "Tavily request failed: Tavily 429: ..." double-prefixed message).
|
|
206
|
-
class TavilyHttpError extends Error {
|
|
207
|
-
}
|
|
208
|
-
const timeoutController = new AbortController();
|
|
209
|
-
const timeoutHandle = setTimeout(() => timeoutController.abort(), DEFAULT_TAVILY_TIMEOUT_MS);
|
|
210
|
-
// Assigned in the `try` and returned *after* the `finally`, rather than returned from inside the
|
|
211
|
-
// `try`. Both forms behave identically, but a return-through-`finally` compiles to three copies of
|
|
212
|
-
// the `finally` body (normal completion, return, throw) and one of those is unreachable here — the
|
|
213
|
-
// `try` cannot fall off its end and the `catch` always throws. Falling through to a single return
|
|
214
|
-
// keeps the cleanup path honestly measurable instead of permanently one-third uncovered.
|
|
215
|
-
let output;
|
|
216
|
-
try {
|
|
217
|
-
const response = await fetch(`${base}/search`, {
|
|
218
|
-
method: 'POST',
|
|
219
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
220
|
-
body: JSON.stringify(body),
|
|
221
|
-
signal: timeoutController.signal,
|
|
222
|
-
});
|
|
223
|
-
if (!response.ok) {
|
|
224
|
-
const text = await response.text().catch(() => '');
|
|
225
|
-
throw new TavilyHttpError(redactSecrets({ input: `Tavily ${response.status}: ${text.slice(0, 200) || 'no body'}` }, { exactSecrets: [apiKey] }));
|
|
226
|
-
}
|
|
227
|
-
const json = (await response.json());
|
|
228
|
-
const answer = typeof json.answer === 'string' ? json.answer : '';
|
|
229
|
-
const rawResults = Array.isArray(json.results) ? json.results : [];
|
|
230
|
-
const sources = [];
|
|
231
|
-
for (const raw of rawResults) {
|
|
232
|
-
const url = typeof raw.url === 'string' ? raw.url : '';
|
|
233
|
-
if (!url)
|
|
234
|
-
continue;
|
|
235
|
-
const publishedAt = typeof raw.published_date === 'string' && raw.published_date.trim() ? raw.published_date.trim() : undefined;
|
|
236
|
-
sources.push({
|
|
237
|
-
title: typeof raw.title === 'string' && raw.title.trim() ? raw.title.trim() : url,
|
|
238
|
-
url,
|
|
239
|
-
snippet: typeof raw.content === 'string' ? raw.content.trim().slice(0, 800) : '',
|
|
240
|
-
provider: 'tavily',
|
|
241
|
-
...(publishedAt ? { publishedAt } : {}),
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
output = { answer, sources };
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
if (!timeoutController.signal.aborted && error instanceof TavilyHttpError) {
|
|
248
|
-
throw error;
|
|
249
|
-
}
|
|
250
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
-
throw new Error(redactSecrets({
|
|
252
|
-
input: timeoutController.signal.aborted
|
|
253
|
-
? `Tavily request timed out after ${DEFAULT_TAVILY_TIMEOUT_MS}ms`
|
|
254
|
-
: `Tavily request failed: ${message}`,
|
|
255
|
-
}, { exactSecrets: [apiKey] }));
|
|
256
|
-
}
|
|
257
|
-
finally {
|
|
258
|
-
clearTimeout(timeoutHandle);
|
|
259
|
-
}
|
|
260
|
-
return output;
|
|
261
|
-
}
|
|
262
161
|
/** Ported verbatim from OD's real `research/index.ts#synthesizeFallbackSummary` — see module doc. Used when Tavily returns no `answer` text so the caller still gets a usable `summary`. */
|
|
263
162
|
function synthesizeFallbackSummary(sources) {
|
|
264
163
|
const lead = sources
|
|
@@ -267,49 +166,65 @@ function synthesizeFallbackSummary(sources) {
|
|
|
267
166
|
.join('\n');
|
|
268
167
|
return `(No provider summary; top snippets follow.)\n${lead}`;
|
|
269
168
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
169
|
+
/** Builds one route over a host-selected provider, preserving the default Tavily validation contract. */
|
|
170
|
+
export function createResearchSearchRoute({ provider }, _optional = {}) {
|
|
171
|
+
const providerId = provider?.id ?? 'tavily';
|
|
172
|
+
return defineJsonRoute({
|
|
173
|
+
method: 'post', path: '/api/research/search', parse: (input) => parseResearchSearch(input, providerId), handle: async ({ input: request, deps }) => {
|
|
174
|
+
const selectedProvider = provider ?? deps.provider ?? createTavilyResearchProvider({}, deps.fetch ? { fetch: deps.fetch } : {});
|
|
175
|
+
const resolveCredentials = deps.resolveCredentials ?? (() => selectedProvider.id === 'tavily' ? defaultResolveCredentials({ env: deps.env ?? process.env }) : Promise.resolve({}));
|
|
176
|
+
const onInternalError = deps.onInternalError ?? defaultInternalErrorSink;
|
|
177
|
+
let credentials;
|
|
178
|
+
try {
|
|
179
|
+
credentials = await resolveCredentials({ providerId: selectedProvider.id });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const correlationId = randomUUID();
|
|
183
|
+
onInternalError({ correlationId, error });
|
|
184
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
185
|
+
}
|
|
186
|
+
const apiKey = credentials.apiKey;
|
|
187
|
+
if (!apiKey) {
|
|
188
|
+
return err({ error: createApiError({ code: 'NOT_CONFIGURED', message: `${selectedProvider.id} provider not configured` }) });
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const raw = await selectedProvider.search({ apiKey, credentials, request }, {});
|
|
192
|
+
// Matches OD's real `NO_RESEARCH_SOURCES` (404) — see module doc. Zero usable sources is a
|
|
193
|
+
// failure, not an empty success, even when the upstream Tavily call itself succeeded.
|
|
194
|
+
if (raw.sources.length === 0) {
|
|
195
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: 'no sources found' }) });
|
|
196
|
+
}
|
|
197
|
+
const result = {
|
|
198
|
+
query: request.query,
|
|
199
|
+
summary: raw.answer || synthesizeFallbackSummary(raw.sources),
|
|
200
|
+
sources: raw.sources,
|
|
201
|
+
provider: selectedProvider.id,
|
|
202
|
+
depth: 'shallow',
|
|
203
|
+
fetchedAt: Date.now(),
|
|
204
|
+
};
|
|
205
|
+
return ok({ value: result });
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const correlationId = randomUUID();
|
|
209
|
+
// A host provider can throw transport text too; keep its configured key out of the sink.
|
|
210
|
+
const safeMessage = redactSecrets({ input: error instanceof Error ? error.message : String(error) }, { exactSecrets: [apiKey] });
|
|
211
|
+
const safeError = new Error(safeMessage);
|
|
212
|
+
if (error instanceof Error) {
|
|
213
|
+
safeError.name = redactSecrets({ input: error.name }, { exactSecrets: [apiKey] });
|
|
214
|
+
if (error.stack === undefined)
|
|
215
|
+
delete safeError.stack;
|
|
216
|
+
else
|
|
217
|
+
safeError.stack = redactSecrets({ input: error.stack }, { exactSecrets: [apiKey] });
|
|
218
|
+
}
|
|
219
|
+
onInternalError({ correlationId, error: safeError });
|
|
220
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
293
221
|
}
|
|
294
|
-
const result = {
|
|
295
|
-
query: request.query,
|
|
296
|
-
summary: raw.answer || synthesizeFallbackSummary(raw.sources),
|
|
297
|
-
sources: raw.sources,
|
|
298
|
-
provider: 'tavily',
|
|
299
|
-
depth: 'shallow',
|
|
300
|
-
fetchedAt: Date.now(),
|
|
301
|
-
};
|
|
302
|
-
return ok({ value: result });
|
|
303
|
-
}
|
|
304
|
-
catch (error) {
|
|
305
|
-
const correlationId = randomUUID();
|
|
306
|
-
onInternalError({ correlationId, error });
|
|
307
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
308
222
|
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
223
|
+
}, { requireSameOrigin: true });
|
|
224
|
+
}
|
|
225
|
+
export const researchSearchRoute = createResearchSearchRoute({});
|
|
311
226
|
/** Mounts `POST /api/research/search` on `app`. A pack's `http(app, services)` calls this directly. */
|
|
312
227
|
export function registerResearchRoutes({ app, deps, adapter }, _optional = {}) {
|
|
313
|
-
mountJsonRoute({ app, spec: researchSearchRoute, deps, adapter });
|
|
228
|
+
mountJsonRoute({ app, spec: deps.provider ? createResearchSearchRoute({ provider: deps.provider }) : researchSearchRoute, deps, adapter });
|
|
314
229
|
}
|
|
315
230
|
//# sourceMappingURL=research.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/daemon",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -139,29 +139,29 @@
|
|
|
139
139
|
"@types/pg": "8.23.1",
|
|
140
140
|
"express": "^4.21.0",
|
|
141
141
|
"@types/express": "^4.17.21",
|
|
142
|
-
"@jini-ai/
|
|
143
|
-
"@jini-ai/
|
|
144
|
-
"@jini-ai/agent-runtime": "^0.4.
|
|
145
|
-
"@jini-ai/
|
|
146
|
-
"@jini-ai/core": "^0.4.
|
|
147
|
-
"@jini-ai/platform": "^0.5.
|
|
148
|
-
"@jini-ai/
|
|
142
|
+
"@jini-ai/chat": "0.5.8",
|
|
143
|
+
"@jini-ai/db": "0.3.2",
|
|
144
|
+
"@jini-ai/agent-runtime": "^0.4.5",
|
|
145
|
+
"@jini-ai/protocol": "^0.4.4",
|
|
146
|
+
"@jini-ai/core": "^0.4.3",
|
|
147
|
+
"@jini-ai/platform": "^0.5.3",
|
|
148
|
+
"@jini-ai/http-kit": "0.4.1"
|
|
149
149
|
},
|
|
150
150
|
"dependencies": {
|
|
151
|
-
"@jini-ai/core": "^0.4.
|
|
152
|
-
"@jini-ai/protocol": "^0.4.
|
|
151
|
+
"@jini-ai/core": "^0.4.3",
|
|
152
|
+
"@jini-ai/protocol": "^0.4.4"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
155
|
"node-pty": "^1.1.0",
|
|
156
|
-
"@jini-ai/db": "^0.
|
|
156
|
+
"@jini-ai/db": "^0.3.2",
|
|
157
157
|
"kysely": "^0.29.6",
|
|
158
158
|
"better-sqlite3": "^13.0.0",
|
|
159
159
|
"@electric-sql/pglite": "0.5.8",
|
|
160
160
|
"pg": "^8.23.0",
|
|
161
161
|
"@jini-ai/http-kit": "^0.4.0",
|
|
162
162
|
"express": "^4.21.0",
|
|
163
|
-
"@jini-ai/agent-runtime": "^0.4.
|
|
164
|
-
"@jini-ai/platform": "^0.5.
|
|
163
|
+
"@jini-ai/agent-runtime": "^0.4.5",
|
|
164
|
+
"@jini-ai/platform": "^0.5.3"
|
|
165
165
|
},
|
|
166
166
|
"peerDependenciesMeta": {
|
|
167
167
|
"node-pty": {
|