@roj-ai/sdk 0.1.29 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/agents/agent.d.ts +28 -0
- package/dist/core/agents/agent.d.ts.map +1 -1
- package/dist/core/agents/agent.js +116 -39
- package/dist/core/agents/agent.js.map +1 -1
- package/dist/core/agents/config.d.ts +9 -0
- package/dist/core/agents/config.d.ts.map +1 -1
- package/dist/core/agents/state.d.ts +9 -0
- package/dist/core/agents/state.d.ts.map +1 -1
- package/dist/core/agents/state.js +4 -0
- package/dist/core/agents/state.js.map +1 -1
- package/dist/core/context/state.d.ts +22 -13
- package/dist/core/context/state.d.ts.map +1 -1
- package/dist/core/context/state.js +78 -22
- package/dist/core/context/state.js.map +1 -1
- package/dist/core/error-recovery.integration.test.d.ts +2 -0
- package/dist/core/error-recovery.integration.test.d.ts.map +1 -0
- package/dist/core/error-recovery.integration.test.js +288 -0
- package/dist/core/error-recovery.integration.test.js.map +1 -0
- package/dist/core/llm/anthropic.d.ts.map +1 -1
- package/dist/core/llm/anthropic.js +6 -1
- package/dist/core/llm/anthropic.js.map +1 -1
- package/dist/core/llm/anthropic.test.js +7 -4
- package/dist/core/llm/anthropic.test.js.map +1 -1
- package/dist/core/llm/message-sanitization.d.ts +10 -0
- package/dist/core/llm/message-sanitization.d.ts.map +1 -0
- package/dist/core/llm/message-sanitization.js +53 -0
- package/dist/core/llm/message-sanitization.js.map +1 -0
- package/dist/core/llm/message-sanitization.test.d.ts +2 -0
- package/dist/core/llm/message-sanitization.test.d.ts.map +1 -0
- package/dist/core/llm/message-sanitization.test.js +181 -0
- package/dist/core/llm/message-sanitization.test.js.map +1 -0
- package/dist/core/llm/middleware.d.ts +3 -3
- package/dist/core/llm/middleware.js +3 -3
- package/dist/core/llm/openrouter-mapping.test.js +21 -3
- package/dist/core/llm/openrouter-mapping.test.js.map +1 -1
- package/dist/core/llm/openrouter.d.ts.map +1 -1
- package/dist/core/llm/openrouter.js +6 -1
- package/dist/core/llm/openrouter.js.map +1 -1
- package/dist/core/llm/provider.d.ts +13 -10
- package/dist/core/llm/provider.d.ts.map +1 -1
- package/dist/core/llm/provider.js.map +1 -1
- package/dist/core/llm/state.d.ts +10 -0
- package/dist/core/llm/state.d.ts.map +1 -1
- package/dist/core/llm/state.js +9 -0
- package/dist/core/llm/state.js.map +1 -1
- package/dist/core/plugins/plugin-builder.d.ts +2 -0
- package/dist/core/plugins/plugin-builder.d.ts.map +1 -1
- package/dist/core/plugins/plugin-builder.js.map +1 -1
- package/dist/core/sessions/session.d.ts.map +1 -1
- package/dist/core/sessions/session.js +6 -0
- package/dist/core/sessions/session.js.map +1 -1
- package/dist/core/sessions/state.d.ts.map +1 -1
- package/dist/core/sessions/state.js +43 -5
- package/dist/core/sessions/state.js.map +1 -1
- package/dist/plugins/context-compact/context-compact.integration.test.js +36 -20
- package/dist/plugins/context-compact/context-compact.integration.test.js.map +1 -1
- package/dist/plugins/context-compact/context-compactor.d.ts.map +1 -1
- package/dist/plugins/context-compact/context-compactor.js +54 -9
- package/dist/plugins/context-compact/context-compactor.js.map +1 -1
- package/dist/plugins/context-compact/context-compactor.test.js +127 -4
- package/dist/plugins/context-compact/context-compactor.test.js.map +1 -1
- package/dist/plugins/limits-guard/config.d.ts +2 -0
- package/dist/plugins/limits-guard/config.d.ts.map +1 -1
- package/dist/plugins/limits-guard/limit-guard.d.ts +1 -0
- package/dist/plugins/limits-guard/limit-guard.d.ts.map +1 -1
- package/dist/plugins/limits-guard/limit-guard.js +11 -1
- package/dist/plugins/limits-guard/limit-guard.js.map +1 -1
- package/dist/plugins/limits-guard/limit-guard.test.js +9 -0
- package/dist/plugins/limits-guard/limit-guard.test.js.map +1 -1
- package/dist/plugins/limits-guard/limits-guard.integration.test.js +216 -1
- package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +1 -1
- package/dist/plugins/limits-guard/plugin.d.ts +3 -1
- package/dist/plugins/limits-guard/plugin.d.ts.map +1 -1
- package/dist/plugins/limits-guard/plugin.js +48 -2
- package/dist/plugins/limits-guard/plugin.js.map +1 -1
- package/dist/plugins/uploads/plugin.d.ts.map +1 -1
- package/dist/plugins/uploads/plugin.js +10 -1
- package/dist/plugins/uploads/plugin.js.map +1 -1
- package/dist/plugins/uploads/uploads.integration.test.js +74 -28
- package/dist/plugins/uploads/uploads.integration.test.js.map +1 -1
- package/dist/testing/test-harness.d.ts +8 -0
- package/dist/testing/test-harness.d.ts.map +1 -1
- package/dist/testing/test-harness.js +18 -0
- package/dist/testing/test-harness.js.map +1 -1
- package/dist/transport/http/middleware/error-handler.d.ts +1 -1
- package/package.json +2 -2
- package/src/core/agents/agent.ts +124 -36
- package/src/core/agents/config.ts +6 -0
- package/src/core/agents/state.ts +9 -0
- package/src/core/context/state.ts +86 -27
- package/src/core/error-recovery.integration.test.ts +343 -0
- package/src/core/llm/anthropic.test.ts +7 -4
- package/src/core/llm/anthropic.ts +6 -1
- package/src/core/llm/message-sanitization.test.ts +217 -0
- package/src/core/llm/message-sanitization.ts +70 -0
- package/src/core/llm/middleware.ts +3 -3
- package/src/core/llm/openrouter-mapping.test.ts +24 -3
- package/src/core/llm/openrouter.ts +7 -8
- package/src/core/llm/provider.ts +14 -10
- package/src/core/llm/state.ts +10 -0
- package/src/core/plugins/plugin-builder.ts +6 -2
- package/src/core/sessions/session.ts +6 -0
- package/src/core/sessions/state.ts +48 -9
- package/src/plugins/context-compact/context-compact.integration.test.ts +38 -21
- package/src/plugins/context-compact/context-compactor.test.ts +150 -4
- package/src/plugins/context-compact/context-compactor.ts +64 -10
- package/src/plugins/limits-guard/config.ts +2 -0
- package/src/plugins/limits-guard/limit-guard.test.ts +13 -0
- package/src/plugins/limits-guard/limit-guard.ts +13 -1
- package/src/plugins/limits-guard/limits-guard.integration.test.ts +246 -1
- package/src/plugins/limits-guard/plugin.ts +53 -3
- package/src/plugins/uploads/plugin.ts +11 -1
- package/src/plugins/uploads/uploads.integration.test.ts +82 -28
- package/src/testing/test-harness.ts +20 -0
package/src/core/agents/agent.ts
CHANGED
|
@@ -89,6 +89,8 @@ export interface AgentConfig<TInput = unknown> {
|
|
|
89
89
|
* full re-uploads. Omit (or '5m') for the standard tier.
|
|
90
90
|
*/
|
|
91
91
|
cacheTtl?: '5m' | '1h'
|
|
92
|
+
/** Outer backoff between error-resume cycles (see BaseAgentConfig.errorResumeBackoff). */
|
|
93
|
+
errorResumeBackoff?: { baseDelayMs?: number; maxDelayMs?: number }
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
/**
|
|
@@ -154,6 +156,7 @@ export class Agent {
|
|
|
154
156
|
|
|
155
157
|
// Scheduler state (embedded)
|
|
156
158
|
private debounceTimer?: ReturnType<typeof setTimeout>
|
|
159
|
+
private errorRetryTimer?: ReturnType<typeof setTimeout>
|
|
157
160
|
private processing = false
|
|
158
161
|
private scheduled = false
|
|
159
162
|
private pendingReschedule = false
|
|
@@ -241,18 +244,34 @@ export class Agent {
|
|
|
241
244
|
})
|
|
242
245
|
for (const toolCall of agentState.pendingToolCalls) {
|
|
243
246
|
await this.executeToolCall(toolCall)
|
|
247
|
+
// A tool hook may have paused the agent — stop the turn here instead of
|
|
248
|
+
// running the remaining tools (they stay pending and run after resume).
|
|
249
|
+
const currentState = this.state
|
|
250
|
+
if (!currentState || currentState.status === 'paused') return
|
|
251
|
+
if (this.abortController.signal.aborted) return
|
|
244
252
|
}
|
|
245
253
|
// Schedule re-entry via debounce after tool execution
|
|
246
254
|
// (allows debounce callback to wait for child responses, etc.)
|
|
247
255
|
this.scheduleProcessing()
|
|
248
256
|
return
|
|
249
257
|
|
|
250
|
-
case 'resume_from_error':
|
|
258
|
+
case 'resume_from_error': {
|
|
259
|
+
// Outer backoff between error-resume cycles: after an inference exhausts
|
|
260
|
+
// its LLM retries on a retryable error, the dequeue token stays unconsumed
|
|
261
|
+
// (preserve-for-retry), so decide() would re-enter infer immediately and
|
|
262
|
+
// spin without limit during a provider outage. Yield instead and come back
|
|
263
|
+
// when the backoff elapses; a session restart resets the episode.
|
|
264
|
+
const backoffRemainingMs = this.errorResumeDelayMs(agentState)
|
|
265
|
+
if (backoffRemainingMs > 0) {
|
|
266
|
+
this.scheduleErrorRetry(backoffRemainingMs)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
251
269
|
await this.store.emit(withSessionId(
|
|
252
270
|
this.store.sessionId,
|
|
253
271
|
agentEvents.create('agent_resumed', { agentId: this.id }),
|
|
254
272
|
))
|
|
255
273
|
continue
|
|
274
|
+
}
|
|
256
275
|
|
|
257
276
|
case 'infer':
|
|
258
277
|
await this.runInference(agentState)
|
|
@@ -377,6 +396,10 @@ export class Agent {
|
|
|
377
396
|
// AbortError may be thrown synchronously by abort signal listeners
|
|
378
397
|
}
|
|
379
398
|
this.cancelSchedule()
|
|
399
|
+
if (this.errorRetryTimer) {
|
|
400
|
+
clearTimeout(this.errorRetryTimer)
|
|
401
|
+
this.errorRetryTimer = undefined
|
|
402
|
+
}
|
|
380
403
|
}
|
|
381
404
|
|
|
382
405
|
/**
|
|
@@ -477,6 +500,39 @@ export class Agent {
|
|
|
477
500
|
return 'idle'
|
|
478
501
|
}
|
|
479
502
|
|
|
503
|
+
private static readonly DEFAULT_ERROR_RESUME_BASE_DELAY_MS = 5_000
|
|
504
|
+
private static readonly DEFAULT_ERROR_RESUME_MAX_DELAY_MS = 60_000
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Remaining wait before an errored agent may resume. Exponential in the number
|
|
508
|
+
* of consecutive failures, measured from the last failure; 0 = resume now.
|
|
509
|
+
* Capped rather than ceilinged: the preserved message keeps retrying at the max
|
|
510
|
+
* delay until the provider recovers — dropping it would lose user work.
|
|
511
|
+
*/
|
|
512
|
+
private errorResumeDelayMs(state: AgentState): number {
|
|
513
|
+
const failures = state.consecutiveInferenceFailures ?? 0
|
|
514
|
+
const lastFailureAt = state.lastInferenceFailureAt
|
|
515
|
+
if (failures === 0 || lastFailureAt === undefined) return 0
|
|
516
|
+
const base = this.config.errorResumeBackoff?.baseDelayMs ?? Agent.DEFAULT_ERROR_RESUME_BASE_DELAY_MS
|
|
517
|
+
const max = this.config.errorResumeBackoff?.maxDelayMs ?? Agent.DEFAULT_ERROR_RESUME_MAX_DELAY_MS
|
|
518
|
+
const delay = Math.min(base * 2 ** (failures - 1), max)
|
|
519
|
+
return Math.max(0, lastFailureAt + delay - Date.now())
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Re-run continue() once the error-resume backoff elapses. A concurrent
|
|
524
|
+
* scheduleProcessing() (new message) is harmless — continue() re-checks the
|
|
525
|
+
* remaining backoff and re-arms if it hasn't elapsed yet.
|
|
526
|
+
*/
|
|
527
|
+
private scheduleErrorRetry(delayMs: number): void {
|
|
528
|
+
if (this.errorRetryTimer) return
|
|
529
|
+
if (this.store.isClosed()) return
|
|
530
|
+
this.errorRetryTimer = setTimeout(() => {
|
|
531
|
+
this.errorRetryTimer = undefined
|
|
532
|
+
this.scheduleProcessing()
|
|
533
|
+
}, delayMs)
|
|
534
|
+
}
|
|
535
|
+
|
|
480
536
|
private static readonly MAX_INFERENCE_RETRIES = 3
|
|
481
537
|
|
|
482
538
|
/** Max retries for empty-stop LLM responses (provider glitch). Total attempts = this + 1. */
|
|
@@ -671,15 +727,7 @@ export class Agent {
|
|
|
671
727
|
// same message into a doomed retry loop. Retryable errors (rate_limit,
|
|
672
728
|
// server_error, network_error, timeout) keep the preserve-for-retry semantics.
|
|
673
729
|
if (!isRetryableLLMError(llmResponse.error)) {
|
|
674
|
-
|
|
675
|
-
if (errorAgentState) {
|
|
676
|
-
const errorCtx = this.buildAgentContext(errorAgentState)
|
|
677
|
-
for (const dequeued of pluginDequeued) {
|
|
678
|
-
if (!dequeued.plugin.dequeue) continue
|
|
679
|
-
const pluginCtx = this.buildPluginHookContext(dequeued.plugin, errorCtx)
|
|
680
|
-
await dequeued.plugin.dequeue.markConsumed(pluginCtx, dequeued.token)
|
|
681
|
-
}
|
|
682
|
-
}
|
|
730
|
+
await this.consumeDequeuedTokens(pluginDequeued)
|
|
683
731
|
}
|
|
684
732
|
|
|
685
733
|
// 4a. Inference failed — emit inference_failed without marking plugin messages
|
|
@@ -694,27 +742,22 @@ export class Agent {
|
|
|
694
742
|
llmCallId,
|
|
695
743
|
}),
|
|
696
744
|
))
|
|
697
|
-
// Notify plugins (e.g. mailbox sends error message to parent)
|
|
745
|
+
// Notify plugins (e.g. mailbox sends error message to parent). For retryable
|
|
746
|
+
// errors, notify only on the first failure of an episode — the preserved
|
|
747
|
+
// token means each error-resume cycle re-fails here, and one parent
|
|
748
|
+
// notification per cycle would flood the parent during a provider outage.
|
|
698
749
|
const errorState = this.state
|
|
699
|
-
|
|
750
|
+
const isRepeatRetryableFailure = isRetryableLLMError(llmResponse.error)
|
|
751
|
+
&& (errorState?.consecutiveInferenceFailures ?? 1) > 1
|
|
752
|
+
if (errorState && !isRepeatRetryableFailure) {
|
|
700
753
|
await this.executeOnError(errorState, llmResponse.error.message)
|
|
701
754
|
}
|
|
702
755
|
return
|
|
703
756
|
}
|
|
704
757
|
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
|
|
708
|
-
const currentAgentState = this.state
|
|
709
|
-
if (currentAgentState) {
|
|
710
|
-
const ctx = this.buildAgentContext(currentAgentState)
|
|
711
|
-
for (const dequeued of pluginDequeued) {
|
|
712
|
-
if (!dequeued.plugin.dequeue) continue
|
|
713
|
-
const pluginCtx = this.buildPluginHookContext(dequeued.plugin, ctx)
|
|
714
|
-
await dequeued.plugin.dequeue.markConsumed(pluginCtx, dequeued.token)
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
758
|
+
// Consumption of dequeue tokens is deferred to the commit points below — after
|
|
759
|
+
// the afterInference retry decision is final. Consuming here would strand the
|
|
760
|
+
// message if a hook requests a retry (the recursion could no longer re-collect it).
|
|
718
761
|
|
|
719
762
|
// 4c. Sanitize response to prevent hallucination
|
|
720
763
|
const sanitized = sanitizeLLMResponse(llmResponse.value.content)
|
|
@@ -742,12 +785,13 @@ export class Agent {
|
|
|
742
785
|
if (postInferenceState) {
|
|
743
786
|
agentState = postInferenceState
|
|
744
787
|
}
|
|
745
|
-
const
|
|
788
|
+
const dequeuedSourcePlugins = pluginDequeued.map((dequeued) => dequeued.plugin.name)
|
|
789
|
+
const afterResult = await this.executeAfterInference(agentState, response, dequeuedSourcePlugins)
|
|
746
790
|
if (afterResult !== null) {
|
|
747
791
|
if (afterResult.action === 'pause') {
|
|
748
|
-
//
|
|
749
|
-
// before pausing so pendingMessages move to conversationHistory.
|
|
792
|
+
// Commit the turn before pausing so pendingMessages move to conversationHistory.
|
|
750
793
|
await this.emitInferenceCompleted(response, llmCallId, llmResponse.value.metrics)
|
|
794
|
+
await this.consumeDequeuedTokens(pluginDequeued)
|
|
751
795
|
await this.emitHandlerPause(afterResult.reason)
|
|
752
796
|
return
|
|
753
797
|
}
|
|
@@ -758,6 +802,14 @@ export class Agent {
|
|
|
758
802
|
retryCount,
|
|
759
803
|
})
|
|
760
804
|
} else {
|
|
805
|
+
// Roll back the staged turn (clears pendingMessages appended by
|
|
806
|
+
// inference_started) so the retry's inference_started doesn't
|
|
807
|
+
// double-append. Tokens are still unconsumed, so the recursion
|
|
808
|
+
// re-collects the same messages — nothing is lost or duplicated.
|
|
809
|
+
await this.store.emit(withSessionId(
|
|
810
|
+
this.store.sessionId,
|
|
811
|
+
llmEvents.create('inference_retried', { agentId: this.id }),
|
|
812
|
+
))
|
|
761
813
|
// Retry inference - decrement turn number and recursively call with fresh state
|
|
762
814
|
this.turnNumber--
|
|
763
815
|
const freshState = this.state
|
|
@@ -773,6 +825,11 @@ export class Agent {
|
|
|
773
825
|
// 4d. Inference completed
|
|
774
826
|
// Tool calls will be executed in the next continue() cycle
|
|
775
827
|
await this.emitInferenceCompleted(response, llmCallId, llmResponse.value.metrics)
|
|
828
|
+
|
|
829
|
+
// Consume dequeue tokens after the commit: a crash in the gap re-delivers the
|
|
830
|
+
// message on restart (at-least-once) instead of dropping it (consume-first
|
|
831
|
+
// would lose the message — session_restarted wipes the staged pendingMessages).
|
|
832
|
+
await this.consumeDequeuedTokens(pluginDequeued)
|
|
776
833
|
}
|
|
777
834
|
|
|
778
835
|
/**
|
|
@@ -913,17 +970,18 @@ export class Agent {
|
|
|
913
970
|
? { isError: false, content: result.value }
|
|
914
971
|
: { isError: true, content: result.error.message }
|
|
915
972
|
|
|
916
|
-
// afterToolCall handler - can modify result or pause
|
|
973
|
+
// afterToolCall handler - can modify result or pause.
|
|
974
|
+
// On 'pause' the agent_paused event was already emitted by the hook dispatch;
|
|
975
|
+
// still fall through to emit the result event below — the tool's side effects
|
|
976
|
+
// already ran, so the committed tool_use must get its tool_result (otherwise
|
|
977
|
+
// the call would re-execute on resume and the history would carry an unpaired
|
|
978
|
+
// tool_use). The tool_completed/tool_failed reducers preserve the paused
|
|
979
|
+
// status, and the tool_exec loop stops before the next tool.
|
|
917
980
|
const currentAgentState = this.state
|
|
918
981
|
if (currentAgentState) {
|
|
919
982
|
const afterResult = await this.executeAfterToolCall(currentAgentState, effectiveToolCall, toolResult)
|
|
920
|
-
if (afterResult !== null) {
|
|
921
|
-
|
|
922
|
-
return
|
|
923
|
-
}
|
|
924
|
-
if (afterResult.action === 'modify') {
|
|
925
|
-
toolResult = afterResult.result
|
|
926
|
-
}
|
|
983
|
+
if (afterResult !== null && afterResult.action === 'modify') {
|
|
984
|
+
toolResult = afterResult.result
|
|
927
985
|
}
|
|
928
986
|
}
|
|
929
987
|
|
|
@@ -1168,6 +1226,7 @@ export class Agent {
|
|
|
1168
1226
|
private async executeAfterInference(
|
|
1169
1227
|
agentState: AgentState,
|
|
1170
1228
|
response: LLMResponse,
|
|
1229
|
+
dequeuedSourcePlugins: readonly string[],
|
|
1171
1230
|
): Promise<AfterInferenceResult> {
|
|
1172
1231
|
this.logger.debug('Executing afterInference handlers', {
|
|
1173
1232
|
agentId: this.id,
|
|
@@ -1184,6 +1243,7 @@ export class Agent {
|
|
|
1184
1243
|
...this.buildPluginHookContext(plugin, agentContext),
|
|
1185
1244
|
response,
|
|
1186
1245
|
turnNumber: this.turnNumber,
|
|
1246
|
+
dequeuedSourcePlugins,
|
|
1187
1247
|
}
|
|
1188
1248
|
const result = await plugin.agentHooks.afterInference(ctx)
|
|
1189
1249
|
if (result === null) {
|
|
@@ -1666,6 +1726,34 @@ export class Agent {
|
|
|
1666
1726
|
|
|
1667
1727
|
return collected
|
|
1668
1728
|
}
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Mark dequeued plugin tokens consumed. Called at turn commit (after
|
|
1732
|
+
* inference_completed, so a crash in the gap re-delivers instead of losing
|
|
1733
|
+
* the message) and to intentionally drop a doomed message on non-retryable
|
|
1734
|
+
* failures.
|
|
1735
|
+
*/
|
|
1736
|
+
private async consumeDequeuedTokens(
|
|
1737
|
+
pluginDequeued: Array<{ plugin: ConfiguredPlugin; messages: LLMMessage[]; token: unknown }>,
|
|
1738
|
+
): Promise<void> {
|
|
1739
|
+
const currentAgentState = this.state
|
|
1740
|
+
if (!currentAgentState) return
|
|
1741
|
+
const ctx = this.buildAgentContext(currentAgentState)
|
|
1742
|
+
for (const dequeued of pluginDequeued) {
|
|
1743
|
+
if (!dequeued.plugin.dequeue) continue
|
|
1744
|
+
const pluginCtx = this.buildPluginHookContext(dequeued.plugin, ctx)
|
|
1745
|
+
await dequeued.plugin.dequeue.markConsumed(pluginCtx, dequeued.token)
|
|
1746
|
+
}
|
|
1747
|
+
if (pluginDequeued.length > 0) {
|
|
1748
|
+
await this.store.emit(withSessionId(
|
|
1749
|
+
this.store.sessionId,
|
|
1750
|
+
agentEvents.create('agent_input_consumed', {
|
|
1751
|
+
agentId: this.id,
|
|
1752
|
+
sourcePlugins: pluginDequeued.map((dequeued) => dequeued.plugin.name),
|
|
1753
|
+
}),
|
|
1754
|
+
))
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1669
1757
|
}
|
|
1670
1758
|
|
|
1671
1759
|
// ============================================================================
|
|
@@ -59,6 +59,12 @@ export interface BaseAgentConfig<TInput = unknown> {
|
|
|
59
59
|
* to the standard 5-minute tier.
|
|
60
60
|
*/
|
|
61
61
|
cacheTtl?: '5m' | '1h'
|
|
62
|
+
/**
|
|
63
|
+
* Outer backoff between error-resume cycles after an inference exhausts its
|
|
64
|
+
* LLM retries on a retryable error (the message stays queued and is retried).
|
|
65
|
+
* Defaults: baseDelayMs 5000, maxDelayMs 60000.
|
|
66
|
+
*/
|
|
67
|
+
errorResumeBackoff?: { baseDelayMs?: number; maxDelayMs?: number }
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/**
|
package/src/core/agents/state.ts
CHANGED
|
@@ -65,6 +65,10 @@ export const agentEvents = createEventsFactory({
|
|
|
65
65
|
agent_resumed: z4.object({
|
|
66
66
|
agentId: agentIdSchema,
|
|
67
67
|
}),
|
|
68
|
+
agent_input_consumed: z4.object({
|
|
69
|
+
agentId: agentIdSchema,
|
|
70
|
+
sourcePlugins: z4.array(z4.string()),
|
|
71
|
+
}),
|
|
68
72
|
communicator_linked: z4.object({
|
|
69
73
|
communicatorId: agentIdSchema,
|
|
70
74
|
orchestratorId: agentIdSchema,
|
|
@@ -85,6 +89,7 @@ export type HandlerCompletedEvent = (typeof agentEvents)['Events']['handler_comp
|
|
|
85
89
|
export type PreambleAddedEvent = (typeof agentEvents)['Events']['preamble_added']
|
|
86
90
|
export type AgentPausedEvent = (typeof agentEvents)['Events']['agent_paused']
|
|
87
91
|
export type AgentResumedEvent = (typeof agentEvents)['Events']['agent_resumed']
|
|
92
|
+
export type AgentInputConsumedEvent = (typeof agentEvents)['Events']['agent_input_consumed']
|
|
88
93
|
export type CommunicatorLinkedEvent = (typeof agentEvents)['Events']['communicator_linked']
|
|
89
94
|
export type AgentConversationSplicedEvent = (typeof agentEvents)['Events']['agent_conversation_spliced']
|
|
90
95
|
|
|
@@ -211,6 +216,10 @@ export interface AgentState {
|
|
|
211
216
|
pauseMessage?: string
|
|
212
217
|
/** Metrics from the most recent completed inference — used by plugins (e.g. context-compact) to size context against the provider-reported truth. */
|
|
213
218
|
lastInferenceMetrics?: LLMMetrics
|
|
219
|
+
/** Consecutive inference_failed count since the last committed turn — drives the outer error-resume backoff. */
|
|
220
|
+
consecutiveInferenceFailures?: number
|
|
221
|
+
/** Timestamp of the most recent inference_failed — drives the outer error-resume backoff. */
|
|
222
|
+
lastInferenceFailureAt?: number
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
// ============================================================================
|
|
@@ -1,18 +1,98 @@
|
|
|
1
1
|
import z4 from 'zod/v4'
|
|
2
2
|
import { agentIdSchema } from '~/core/agents'
|
|
3
|
+
import type { LLMMessage } from '~/core/agents/state.js'
|
|
3
4
|
import { createEventsFactory } from '~/core/events/types'
|
|
5
|
+
import { toolCallIdSchema } from '~/core/tools/schema.js'
|
|
6
|
+
import { messageIdSchema } from '~/plugins/mailbox/schema.js'
|
|
4
7
|
|
|
5
8
|
// ============================================================================
|
|
6
9
|
// Supporting types for context events
|
|
7
10
|
// ============================================================================
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
|
-
* Conversation message stored in event for reconstruction.
|
|
13
|
+
* Conversation message stored in the event for lossless reconstruction.
|
|
11
14
|
*/
|
|
12
|
-
export type CompactedConversationMessage =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
export type CompactedConversationMessage = LLMMessage
|
|
16
|
+
|
|
17
|
+
const cacheControlSchema = z4.object({
|
|
18
|
+
type: z4.literal('ephemeral'),
|
|
19
|
+
ttl: z4.enum(['5m', '1h']).optional(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const contentItemSchema = z4.discriminatedUnion('type', [
|
|
23
|
+
z4.object({
|
|
24
|
+
type: z4.literal('text'),
|
|
25
|
+
text: z4.string(),
|
|
26
|
+
cacheControl: z4.object({
|
|
27
|
+
type: z4.literal('ephemeral'),
|
|
28
|
+
}).optional(),
|
|
29
|
+
}),
|
|
30
|
+
z4.object({
|
|
31
|
+
type: z4.literal('image_url'),
|
|
32
|
+
imageUrl: z4.object({
|
|
33
|
+
url: z4.string(),
|
|
34
|
+
detail: z4.enum(['auto', 'low', 'high']).optional(),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
const messageContentSchema = z4.union([
|
|
40
|
+
z4.string(),
|
|
41
|
+
z4.array(contentItemSchema),
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
export const compactedConversationMessageSchema: z4.ZodType<LLMMessage> = z4.discriminatedUnion('role', [
|
|
45
|
+
z4.object({
|
|
46
|
+
role: z4.literal('user'),
|
|
47
|
+
content: messageContentSchema,
|
|
48
|
+
sourceMessageIds: z4.array(messageIdSchema).optional(),
|
|
49
|
+
cacheControl: cacheControlSchema.optional(),
|
|
50
|
+
}),
|
|
51
|
+
z4.object({
|
|
52
|
+
role: z4.literal('assistant'),
|
|
53
|
+
content: z4.string(),
|
|
54
|
+
toolCalls: z4.array(z4.object({
|
|
55
|
+
id: toolCallIdSchema,
|
|
56
|
+
name: z4.string(),
|
|
57
|
+
input: z4.unknown(),
|
|
58
|
+
})).optional(),
|
|
59
|
+
cacheControl: cacheControlSchema.optional(),
|
|
60
|
+
}),
|
|
61
|
+
z4.object({
|
|
62
|
+
role: z4.literal('tool'),
|
|
63
|
+
content: messageContentSchema,
|
|
64
|
+
toolCallId: toolCallIdSchema,
|
|
65
|
+
toolName: z4.string().optional(),
|
|
66
|
+
isError: z4.boolean().optional(),
|
|
67
|
+
timestamp: z4.number().optional(),
|
|
68
|
+
cacheControl: cacheControlSchema.optional(),
|
|
69
|
+
}),
|
|
70
|
+
z4.object({
|
|
71
|
+
role: z4.literal('system'),
|
|
72
|
+
content: z4.string(),
|
|
73
|
+
cacheControl: cacheControlSchema.optional(),
|
|
74
|
+
}),
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
export const contextCompactedEventSchema = z4.object({
|
|
78
|
+
agentId: agentIdSchema,
|
|
79
|
+
compactedContent: z4.string(),
|
|
80
|
+
/**
|
|
81
|
+
* Display-only snapshot of the messages that were summarized away (the
|
|
82
|
+
* "input" of the compaction). Not used for conversation reconstruction —
|
|
83
|
+
* that's driven by newConversationHistory. Optional for backward
|
|
84
|
+
* compatibility with events emitted before this field existed.
|
|
85
|
+
*/
|
|
86
|
+
originalMessages: z4.array(z4.object({
|
|
87
|
+
role: z4.enum(['user', 'assistant', 'system']),
|
|
88
|
+
content: z4.string(),
|
|
89
|
+
})).optional(),
|
|
90
|
+
newConversationHistory: z4.array(compactedConversationMessageSchema),
|
|
91
|
+
originalTokens: z4.number(),
|
|
92
|
+
compactedTokens: z4.number(),
|
|
93
|
+
messagesRemoved: z4.number(),
|
|
94
|
+
historyPath: z4.string().optional(),
|
|
95
|
+
})
|
|
16
96
|
|
|
17
97
|
// ============================================================================
|
|
18
98
|
// Context events
|
|
@@ -20,28 +100,7 @@ export type CompactedConversationMessage = {
|
|
|
20
100
|
|
|
21
101
|
export const contextEvents = createEventsFactory({
|
|
22
102
|
events: {
|
|
23
|
-
context_compacted:
|
|
24
|
-
agentId: agentIdSchema,
|
|
25
|
-
compactedContent: z4.string(),
|
|
26
|
-
/**
|
|
27
|
-
* Display-only snapshot of the messages that were summarized away (the
|
|
28
|
-
* "input" of the compaction). Not used for conversation reconstruction —
|
|
29
|
-
* that's driven by {@link newConversationHistory}. Optional for backward
|
|
30
|
-
* compatibility with events emitted before this field existed.
|
|
31
|
-
*/
|
|
32
|
-
originalMessages: z4.array(z4.object({
|
|
33
|
-
role: z4.enum(['user', 'assistant', 'system']),
|
|
34
|
-
content: z4.string(),
|
|
35
|
-
})).optional(),
|
|
36
|
-
newConversationHistory: z4.array(z4.object({
|
|
37
|
-
role: z4.enum(['user', 'assistant', 'system']),
|
|
38
|
-
content: z4.string(),
|
|
39
|
-
})),
|
|
40
|
-
originalTokens: z4.number(),
|
|
41
|
-
compactedTokens: z4.number(),
|
|
42
|
-
messagesRemoved: z4.number(),
|
|
43
|
-
historyPath: z4.string().optional(),
|
|
44
|
-
}),
|
|
103
|
+
context_compacted: contextCompactedEventSchema,
|
|
45
104
|
},
|
|
46
105
|
})
|
|
47
106
|
|