@letta-ai/letta-code 0.34.2 → 0.34.4
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/gateway-core.js +166 -621
- package/dist/gateway-core.js.map +8 -13
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp-oauth.js +2 -2
- package/dist/mcp-oauth.js.map +1 -1
- package/dist/memory-constraints.js +47 -13
- package/dist/memory-constraints.js.map +3 -3
- package/dist/types/agent/memory-constraints.d.ts +2 -0
- package/dist/types/agent/memory-constraints.d.ts.map +1 -1
- package/dist/types/agent/subagents/manager.d.ts.map +1 -1
- package/dist/types/agent/subagents/sandbox.d.ts +6 -0
- package/dist/types/agent/subagents/sandbox.d.ts.map +1 -1
- package/dist/types/agent/subagents/subagent-depth.d.ts +15 -0
- package/dist/types/agent/subagents/subagent-depth.d.ts.map +1 -0
- package/dist/types/agent/subagents/subagent-launcher.d.ts.map +1 -1
- package/dist/types/agent/subagents/subagent-process.d.ts +7 -0
- package/dist/types/agent/subagents/subagent-process.d.ts.map +1 -1
- package/dist/types/backend/api/agent-message.d.ts.map +1 -1
- package/dist/types/backend/dev/local-provider-errors.d.ts.map +1 -1
- package/dist/types/backend/local/local-pi-credential-store.d.ts +4 -3
- package/dist/types/backend/local/local-pi-credential-store.d.ts.map +1 -1
- package/dist/types/backend/local/local-provider-auth-store.d.ts +7 -0
- package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
- package/dist/types/channels/gateway-core.d.ts +50 -8
- package/dist/types/channels/gateway-core.d.ts.map +1 -1
- package/dist/types/channels/gateway-sources.d.ts +0 -1
- package/dist/types/channels/gateway-sources.d.ts.map +1 -1
- package/dist/types/channels/message-channel-executor.d.ts +0 -1
- package/dist/types/channels/message-channel-executor.d.ts.map +1 -1
- package/dist/types/channels/message-channel-idempotency.d.ts +5 -14
- package/dist/types/channels/message-channel-idempotency.d.ts.map +1 -1
- package/dist/types/channels/message-channel-tool-definition.d.ts +1 -4
- package/dist/types/channels/message-channel-tool-definition.d.ts.map +1 -1
- package/dist/types/channels/plugin-types.d.ts +2 -3
- package/dist/types/channels/plugin-types.d.ts.map +1 -1
- package/dist/types/channels/types.d.ts +1 -3
- package/dist/types/channels/types.d.ts.map +1 -1
- package/dist/types/cli/helpers/session-context.d.ts +10 -2
- package/dist/types/cli/helpers/session-context.d.ts.map +1 -1
- package/dist/types/headless-super-run-wait.d.ts.map +1 -1
- package/dist/types/mcp-scope.d.ts +5 -0
- package/dist/types/mcp-scope.d.ts.map +1 -0
- package/dist/types/memory-constraints.d.ts +4 -0
- package/dist/types/memory-constraints.d.ts.map +1 -1
- package/dist/types/mods/mod-engine.d.ts.map +1 -1
- package/dist/types/mods/turn-start-cancel.d.ts +17 -1
- package/dist/types/mods/turn-start-cancel.d.ts.map +1 -1
- package/dist/types/reminders/engine.d.ts +2 -0
- package/dist/types/reminders/engine.d.ts.map +1 -1
- package/dist/types/runtime-execution-settings.d.ts +5 -0
- package/dist/types/runtime-execution-settings.d.ts.map +1 -1
- package/dist/types/tools/impl/bash.d.ts.map +1 -1
- package/dist/types/tools/impl/codex-app-server.d.ts.map +1 -1
- package/dist/types/tools/impl/external-coding-agent.d.ts +4 -3
- package/dist/types/tools/impl/external-coding-agent.d.ts.map +1 -1
- package/dist/types/tools/impl/foreground-sleep.d.ts +13 -0
- package/dist/types/tools/impl/foreground-sleep.d.ts.map +1 -0
- package/dist/types/tools/impl/process_manager.d.ts +2 -0
- package/dist/types/tools/impl/process_manager.d.ts.map +1 -1
- package/dist/types/tools/impl/send-agent-message.d.ts.map +1 -1
- package/dist/types/tools/impl/shell-launchers.d.ts.map +1 -1
- package/dist/types/tools/impl/task-foreground.d.ts +21 -0
- package/dist/types/tools/impl/task-foreground.d.ts.map +1 -0
- package/dist/types/tools/impl/task-stop.d.ts +1 -0
- package/dist/types/tools/impl/task-stop.d.ts.map +1 -1
- package/dist/types/tools/impl/task-transcript.d.ts +5 -0
- package/dist/types/tools/impl/task-transcript.d.ts.map +1 -0
- package/dist/types/tools/impl/task.d.ts +1 -1
- package/dist/types/tools/impl/task.d.ts.map +1 -1
- package/dist/types/tools/manager.d.ts +1 -1
- package/dist/types/tools/manager.d.ts.map +1 -1
- package/dist/types/tools/secret-substitution.d.ts +7 -0
- package/dist/types/tools/secret-substitution.d.ts.map +1 -1
- package/dist/types/tools/subagent-tool-policy.d.ts +16 -0
- package/dist/types/tools/subagent-tool-policy.d.ts.map +1 -0
- package/dist/types/tools/toolset-catalog.d.ts +1 -1
- package/dist/types/types/protocol_v2.d.ts +11 -5
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/utils/checkout-readiness.d.ts +1 -1
- package/dist/types/utils/checkout-readiness.d.ts.map +1 -1
- package/dist/types/utils/secrets-store.d.ts +2 -0
- package/dist/types/utils/secrets-store.d.ts.map +1 -1
- package/dist/types/utils/shell-context.d.ts +8 -0
- package/dist/types/utils/shell-context.d.ts.map +1 -1
- package/dist/types/utils/subagent-depth-env.d.ts +9 -0
- package/dist/types/utils/subagent-depth-env.d.ts.map +1 -0
- package/dist/types/websocket/listener/queue.d.ts.map +1 -1
- package/letta.js +1559 -1608
- package/package.json +1 -1
- package/scripts/source-file-size-baseline.json +4 -4
- package/skills/creating-mods/references/events.md +2 -0
- package/skills/self-configuration/SKILL.md +32 -38
- package/dist/types/channels/gateway-assistant-relay.d.ts +0 -37
- package/dist/types/channels/gateway-assistant-relay.d.ts.map +0 -1
- package/dist/types/channels/gateway-registration-policy.d.ts +0 -14
- package/dist/types/channels/gateway-registration-policy.d.ts.map +0 -1
- package/dist/types/channels/gateway-runtime-registration.d.ts +0 -12
- package/dist/types/channels/gateway-runtime-registration.d.ts.map +0 -1
- package/dist/types/channels/gateway-runtime-state.d.ts +0 -48
- package/dist/types/channels/gateway-runtime-state.d.ts.map +0 -1
- package/dist/types/channels/gateway-runtime-tool-scopes.d.ts +0 -26
- package/dist/types/channels/gateway-runtime-tool-scopes.d.ts.map +0 -1
- package/dist/types/channels/gateway-types.d.ts +0 -71
- package/dist/types/channels/gateway-types.d.ts.map +0 -1
- package/dist/types/types/channel-account-protocol.d.ts +0 -14
- package/dist/types/types/channel-account-protocol.d.ts.map +0 -1
package/dist/gateway-core.js
CHANGED
|
@@ -485,117 +485,6 @@ class ChannelControlRequestCoordinator {
|
|
|
485
485
|
return true;
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
|
-
// src/channels/gateway-assistant-relay.ts
|
|
489
|
-
class GatewayAssistantTextAccumulator {
|
|
490
|
-
currentMessageId;
|
|
491
|
-
currentText;
|
|
492
|
-
deltaKeys;
|
|
493
|
-
finalizedMessageIds;
|
|
494
|
-
constructor(state) {
|
|
495
|
-
this.currentMessageId = state?.currentMessageId ?? null;
|
|
496
|
-
this.currentText = state?.currentText ?? "";
|
|
497
|
-
this.deltaKeys = new Set(state?.deltaKeys ?? []);
|
|
498
|
-
this.finalizedMessageIds = new Set(state?.finalizedMessageIds ?? []);
|
|
499
|
-
}
|
|
500
|
-
snapshot() {
|
|
501
|
-
return {
|
|
502
|
-
currentMessageId: this.currentMessageId,
|
|
503
|
-
currentText: this.currentText,
|
|
504
|
-
deltaKeys: [...this.deltaKeys],
|
|
505
|
-
finalizedMessageIds: [...this.finalizedMessageIds]
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
handleDelta(message) {
|
|
509
|
-
const delta = message.delta;
|
|
510
|
-
if (delta.message_type === "assistant_message") {
|
|
511
|
-
const messageId = delta.otid || delta.id;
|
|
512
|
-
if (!messageId || this.finalizedMessageIds.has(messageId))
|
|
513
|
-
return [];
|
|
514
|
-
const finalized = this.currentMessageId && this.currentMessageId !== messageId ? this.finalizeCurrent() : null;
|
|
515
|
-
if (!this.currentMessageId)
|
|
516
|
-
this.currentMessageId = messageId;
|
|
517
|
-
if (!this.deltaKeys.has(message.idempotency_key)) {
|
|
518
|
-
const text = typeof delta.content === "string" ? delta.content : delta.content.map((part) => part.text ?? "").join("");
|
|
519
|
-
this.deltaKeys.add(message.idempotency_key);
|
|
520
|
-
this.currentText += text;
|
|
521
|
-
}
|
|
522
|
-
return finalized ? [finalized] : [];
|
|
523
|
-
}
|
|
524
|
-
if (delta.message_type === "tool_call_message" || delta.message_type === "approval_request_message") {
|
|
525
|
-
const finalized = this.finalizeCurrent();
|
|
526
|
-
return finalized ? [finalized] : [];
|
|
527
|
-
}
|
|
528
|
-
if (delta.message_type === "reasoning_message") {
|
|
529
|
-
const boundaryId = delta.otid || delta.id;
|
|
530
|
-
if (this.currentMessageId && (!boundaryId || boundaryId !== this.currentMessageId)) {
|
|
531
|
-
const finalized = this.finalizeCurrent();
|
|
532
|
-
return finalized ? [finalized] : [];
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return [];
|
|
536
|
-
}
|
|
537
|
-
finalizeCurrent() {
|
|
538
|
-
const messageId = this.currentMessageId;
|
|
539
|
-
if (!messageId)
|
|
540
|
-
return null;
|
|
541
|
-
const text = this.currentText.trim();
|
|
542
|
-
this.currentMessageId = null;
|
|
543
|
-
this.currentText = "";
|
|
544
|
-
this.finalizedMessageIds.add(messageId);
|
|
545
|
-
return text ? { messageId, text } : null;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
function relayFinalizedAssistantMessage(options) {
|
|
549
|
-
if (options.sources.length !== 1 || !options.relay)
|
|
550
|
-
return;
|
|
551
|
-
const warn = (error) => {
|
|
552
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
553
|
-
console.warn(`[channels] Automatic relay failed: ${detail}`);
|
|
554
|
-
};
|
|
555
|
-
try {
|
|
556
|
-
const pending = options.relay({
|
|
557
|
-
text: options.message.text,
|
|
558
|
-
sources: options.sources,
|
|
559
|
-
idempotencyScope: options.idempotencyScope
|
|
560
|
-
});
|
|
561
|
-
if (pending)
|
|
562
|
-
return pending.catch(warn);
|
|
563
|
-
} catch (error) {
|
|
564
|
-
warn(error);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
function stopReasonFromDelta(message) {
|
|
568
|
-
const delta = message.delta;
|
|
569
|
-
return delta.message_type === "stop_reason" && "stop_reason" in delta && typeof delta.stop_reason === "string" ? delta.stop_reason : null;
|
|
570
|
-
}
|
|
571
|
-
function runIdFromDelta(message) {
|
|
572
|
-
const runId = "run_id" in message.delta ? message.delta.run_id : undefined;
|
|
573
|
-
return typeof runId === "string" && runId.length > 0 ? runId : undefined;
|
|
574
|
-
}
|
|
575
|
-
function channelTurnOutcome(stopReason) {
|
|
576
|
-
if (stopReason === "cancelled")
|
|
577
|
-
return "cancelled";
|
|
578
|
-
return stopReason === "end_turn" || stopReason === "tool_rule" ? "completed" : "error";
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// src/channels/gateway-registration-policy.ts
|
|
582
|
-
async function prepareGatewayRegistrationPolicy(options) {
|
|
583
|
-
const automaticRelay = options.override ?? (options.hooks.resolveAssistantRelayPolicy ? await options.hooks.resolveAssistantRelayPolicy(options.runtime, options.sources) : Boolean(options.hooks.relayAssistantText));
|
|
584
|
-
const tool = await options.hooks.buildExternalTool(options.runtime, options.sources, { automaticRelay });
|
|
585
|
-
return { automaticRelay, tool };
|
|
586
|
-
}
|
|
587
|
-
function modelStatusFromRuntimeStart(response, runtime) {
|
|
588
|
-
const agent = response.agent;
|
|
589
|
-
const conversation = response.conversation;
|
|
590
|
-
const agentModel = typeof agent?.model === "string" ? agent.model : response.agent?.llm_config?.model ?? null;
|
|
591
|
-
const conversationModel = typeof conversation?.model === "string" ? conversation.model : null;
|
|
592
|
-
const isAgentScope = runtime.conversation_id === "default";
|
|
593
|
-
return {
|
|
594
|
-
modelHandle: isAgentScope ? agentModel : conversationModel ?? agentModel,
|
|
595
|
-
scope: isAgentScope ? "agent" : "conversation"
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
|
|
599
488
|
// src/channels/gateway-sources.ts
|
|
600
489
|
function sourceRouteKey(source) {
|
|
601
490
|
return [
|
|
@@ -625,229 +514,6 @@ function uniqueRoutedSources(sources) {
|
|
|
625
514
|
function uniqueLifecycleSources(sources) {
|
|
626
515
|
return uniqueSourcesBy(sources, sourceLifecycleKey);
|
|
627
516
|
}
|
|
628
|
-
function channelTagsForSources(sources) {
|
|
629
|
-
return [...new Set(sources.map((source) => `channel:${source.channel}`))];
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// src/channels/gateway-runtime-registration.ts
|
|
633
|
-
async function performGatewayRuntimeRegistration(options) {
|
|
634
|
-
const {
|
|
635
|
-
client,
|
|
636
|
-
hooks,
|
|
637
|
-
state,
|
|
638
|
-
delivery,
|
|
639
|
-
policyOverride,
|
|
640
|
-
externalToolsOverride
|
|
641
|
-
} = options;
|
|
642
|
-
const prepared = externalToolsOverride ? { automaticRelay: policyOverride ?? false, tool: null } : await prepareGatewayRegistrationPolicy({
|
|
643
|
-
hooks,
|
|
644
|
-
runtime: delivery.runtime,
|
|
645
|
-
sources: delivery.sources,
|
|
646
|
-
...policyOverride === undefined ? {} : { override: policyOverride }
|
|
647
|
-
});
|
|
648
|
-
const { automaticRelay, tool } = prepared;
|
|
649
|
-
const externalTools = externalToolsOverride ?? (tool ? [{ tools: [tool] }] : []);
|
|
650
|
-
const conversationTags = channelTagsForSources(delivery.sources);
|
|
651
|
-
const signature = JSON.stringify({
|
|
652
|
-
mode: delivery.defaultPermissionMode ?? null,
|
|
653
|
-
automaticRelay,
|
|
654
|
-
externalTools,
|
|
655
|
-
conversationTags
|
|
656
|
-
});
|
|
657
|
-
if (state.registrationSignature === signature && state.registration) {
|
|
658
|
-
await state.registration;
|
|
659
|
-
return automaticRelay;
|
|
660
|
-
}
|
|
661
|
-
const registration = client.runtimeStart({
|
|
662
|
-
agent_id: delivery.runtime.agent_id,
|
|
663
|
-
conversation_id: delivery.runtime.conversation_id,
|
|
664
|
-
...conversationTags.length > 0 ? { conversation_source_tags: conversationTags } : {},
|
|
665
|
-
...delivery.defaultPermissionMode ? { mode: delivery.defaultPermissionMode } : {},
|
|
666
|
-
recover_approvals: true,
|
|
667
|
-
force_device_status: false,
|
|
668
|
-
wait_for_replay: true,
|
|
669
|
-
preserve_skill_sources: true,
|
|
670
|
-
client_info: { name: "channel-gateway", title: "Channel Gateway" },
|
|
671
|
-
external_tools: externalTools
|
|
672
|
-
}).then((response) => {
|
|
673
|
-
if (!response.success) {
|
|
674
|
-
throw new Error(response.error ?? "Failed to register channel runtime");
|
|
675
|
-
}
|
|
676
|
-
state.modelStatus = modelStatusFromRuntimeStart(response, delivery.runtime);
|
|
677
|
-
});
|
|
678
|
-
state.registrationSignature = signature;
|
|
679
|
-
state.registration = registration;
|
|
680
|
-
try {
|
|
681
|
-
await registration;
|
|
682
|
-
} catch (error) {
|
|
683
|
-
if (state.registration === registration) {
|
|
684
|
-
state.registration = null;
|
|
685
|
-
state.registrationSignature = null;
|
|
686
|
-
}
|
|
687
|
-
throw error;
|
|
688
|
-
}
|
|
689
|
-
return automaticRelay;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// src/channels/gateway-runtime-state.ts
|
|
693
|
-
var MAX_ACCEPTED_CLIENT_MESSAGE_IDS = 2048;
|
|
694
|
-
function gatewayRuntimeKey(runtime) {
|
|
695
|
-
return `${runtime.agent_id}:${runtime.conversation_id}`;
|
|
696
|
-
}
|
|
697
|
-
function hasAgentRuntime(value) {
|
|
698
|
-
return !!value.runtime?.agent_id;
|
|
699
|
-
}
|
|
700
|
-
function hasGatewayRuntimeWork(state) {
|
|
701
|
-
return Boolean(state && (state.active || state.pendingSourcesByClientMessageId.size > 0));
|
|
702
|
-
}
|
|
703
|
-
function rememberAcceptedClientMessageId(state, clientMessageId) {
|
|
704
|
-
state.acceptedClientMessageIds.delete(clientMessageId);
|
|
705
|
-
state.acceptedClientMessageIds.add(clientMessageId);
|
|
706
|
-
if (state.acceptedClientMessageIds.size <= MAX_ACCEPTED_CLIENT_MESSAGE_IDS)
|
|
707
|
-
return;
|
|
708
|
-
const oldest = state.acceptedClientMessageIds.values().next().value;
|
|
709
|
-
if (oldest)
|
|
710
|
-
state.acceptedClientMessageIds.delete(oldest);
|
|
711
|
-
}
|
|
712
|
-
function remainingGatewaySources(state) {
|
|
713
|
-
return uniqueLifecycleSources([
|
|
714
|
-
...state.active?.lifecycleSources ?? [],
|
|
715
|
-
...Array.from(state.pendingSourcesByClientMessageId.values()).flatMap((pending) => pending.sources)
|
|
716
|
-
]);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// src/channels/gateway-runtime-tool-scopes.ts
|
|
720
|
-
var RETRY_DELAY_MS = 1000;
|
|
721
|
-
function runtimeKey(runtime) {
|
|
722
|
-
return `${runtime.agent_id}:${runtime.conversation_id}`;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
class GatewayRuntimeToolScopes {
|
|
726
|
-
client;
|
|
727
|
-
states = new Map;
|
|
728
|
-
retryTimers = new Set;
|
|
729
|
-
closed = false;
|
|
730
|
-
constructor(client) {
|
|
731
|
-
this.client = client;
|
|
732
|
-
}
|
|
733
|
-
create(runtime, sources, automaticRelay, tool) {
|
|
734
|
-
const id = `channel-turn-${crypto.randomUUID()}`;
|
|
735
|
-
const scope = {
|
|
736
|
-
id,
|
|
737
|
-
runtime,
|
|
738
|
-
sources: structuredClone(sources),
|
|
739
|
-
automaticRelay,
|
|
740
|
-
...tool ? { group: { scope_id: id, tools: [structuredClone(tool)] } } : {}
|
|
741
|
-
};
|
|
742
|
-
this.retain(scope);
|
|
743
|
-
return scope;
|
|
744
|
-
}
|
|
745
|
-
retain(scope) {
|
|
746
|
-
this.getState(scope.runtime).scopes.set(scope.id, scope);
|
|
747
|
-
}
|
|
748
|
-
detach(scope) {
|
|
749
|
-
this.states.get(runtimeKey(scope.runtime))?.scopes.delete(scope.id);
|
|
750
|
-
return scope;
|
|
751
|
-
}
|
|
752
|
-
release(scope) {
|
|
753
|
-
this.states.get(runtimeKey(scope.runtime))?.scopes.delete(scope.id);
|
|
754
|
-
}
|
|
755
|
-
resolve(runtime, scopeId) {
|
|
756
|
-
return this.states.get(runtimeKey(runtime))?.scopes.get(scopeId) ?? null;
|
|
757
|
-
}
|
|
758
|
-
selection(scope) {
|
|
759
|
-
return [scope.id];
|
|
760
|
-
}
|
|
761
|
-
hasRetainedScopes(runtime) {
|
|
762
|
-
return (this.states.get(runtimeKey(runtime))?.scopes.size ?? 0) > 0;
|
|
763
|
-
}
|
|
764
|
-
registrationGroups(runtime, includeUnscoped) {
|
|
765
|
-
const state = this.states.get(runtimeKey(runtime));
|
|
766
|
-
if (!state)
|
|
767
|
-
return [];
|
|
768
|
-
const scoped = [...state.scopes.values()].flatMap((scope) => scope.group ? [scope.group] : []);
|
|
769
|
-
return includeUnscoped ? [...scoped, ...state.desiredUnscoped] : scoped;
|
|
770
|
-
}
|
|
771
|
-
setDesiredUnscoped(runtime, groups) {
|
|
772
|
-
this.getState(runtime).desiredUnscoped = groups;
|
|
773
|
-
}
|
|
774
|
-
async updateDesiredUnscoped(updates, isBusy) {
|
|
775
|
-
const immediateByTools = new Map;
|
|
776
|
-
for (const update of updates) {
|
|
777
|
-
for (const runtime of update.runtimes) {
|
|
778
|
-
this.getState(runtime).desiredUnscoped = update.external_tools;
|
|
779
|
-
if (!isBusy(runtime)) {
|
|
780
|
-
const externalTools = this.registrationGroups(runtime, true);
|
|
781
|
-
const signature = JSON.stringify(externalTools);
|
|
782
|
-
const existing = immediateByTools.get(signature);
|
|
783
|
-
if (existing)
|
|
784
|
-
existing.runtimes.push(runtime);
|
|
785
|
-
else
|
|
786
|
-
immediateByTools.set(signature, {
|
|
787
|
-
runtimes: [runtime],
|
|
788
|
-
external_tools: externalTools
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const immediate = [...immediateByTools.values()];
|
|
794
|
-
if (immediate.length === 0)
|
|
795
|
-
return;
|
|
796
|
-
const response = await this.client.runtimeExternalToolsUpdate({
|
|
797
|
-
updates: immediate
|
|
798
|
-
});
|
|
799
|
-
if (!response.success) {
|
|
800
|
-
throw new Error(response.error ?? "Failed to update routed runtime tools");
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
flushWhenIdle(runtime, isBusy, schedule) {
|
|
804
|
-
if (isBusy() || this.closed)
|
|
805
|
-
return;
|
|
806
|
-
schedule(async () => {
|
|
807
|
-
if (isBusy())
|
|
808
|
-
return;
|
|
809
|
-
const response = await this.client.runtimeExternalToolsUpdate({
|
|
810
|
-
updates: [
|
|
811
|
-
{
|
|
812
|
-
runtimes: [runtime],
|
|
813
|
-
external_tools: this.registrationGroups(runtime, true)
|
|
814
|
-
}
|
|
815
|
-
]
|
|
816
|
-
});
|
|
817
|
-
if (!response.success) {
|
|
818
|
-
throw new Error(response.error ?? "Failed to update routed runtime tools");
|
|
819
|
-
}
|
|
820
|
-
}).catch(() => {
|
|
821
|
-
if (this.closed)
|
|
822
|
-
return;
|
|
823
|
-
const timer = setTimeout(() => {
|
|
824
|
-
this.retryTimers.delete(timer);
|
|
825
|
-
this.flushWhenIdle(runtime, isBusy, schedule);
|
|
826
|
-
}, RETRY_DELAY_MS);
|
|
827
|
-
timer.unref?.();
|
|
828
|
-
this.retryTimers.add(timer);
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
forgetRuntime(runtime) {
|
|
832
|
-
this.states.delete(runtimeKey(runtime));
|
|
833
|
-
}
|
|
834
|
-
close() {
|
|
835
|
-
this.closed = true;
|
|
836
|
-
for (const timer of this.retryTimers)
|
|
837
|
-
clearTimeout(timer);
|
|
838
|
-
this.retryTimers.clear();
|
|
839
|
-
this.states.clear();
|
|
840
|
-
}
|
|
841
|
-
getState(runtime) {
|
|
842
|
-
const key = runtimeKey(runtime);
|
|
843
|
-
let state = this.states.get(key);
|
|
844
|
-
if (!state) {
|
|
845
|
-
state = { runtime, scopes: new Map, desiredUnscoped: [] };
|
|
846
|
-
this.states.set(key, state);
|
|
847
|
-
}
|
|
848
|
-
return state;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
517
|
|
|
852
518
|
// src/channels/message-channel-idempotency.ts
|
|
853
519
|
class MessageChannelDuplicateActionError extends Error {
|
|
@@ -860,108 +526,38 @@ class MessageChannelDuplicateActionError extends Error {
|
|
|
860
526
|
function isErrorResult(result) {
|
|
861
527
|
return result.startsWith("Error:");
|
|
862
528
|
}
|
|
863
|
-
function createMessageChannelIdempotencyScope(
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
const inFlightRelayTextDeliveryKeys = new Set;
|
|
867
|
-
const successfulActionKeys = new Set(state?.successfulActionKeys ?? []);
|
|
868
|
-
const successfulTextDeliveryKeys = new Set(state?.successfulTextDeliveryKeys ?? []);
|
|
869
|
-
const successfulRelayTextDeliveryKeys = new Set(state?.successfulRelayTextDeliveryKeys ?? []);
|
|
870
|
-
let lastSuccessfulActionKey = state?.lastSuccessfulActionKey ?? null;
|
|
529
|
+
function createMessageChannelIdempotencyScope() {
|
|
530
|
+
const inFlight = new Map;
|
|
531
|
+
let lastSuccessful = null;
|
|
871
532
|
let latestInvocation = 0;
|
|
872
|
-
const addInFlightTextDelivery = (key) => {
|
|
873
|
-
if (key) {
|
|
874
|
-
inFlightTextDeliveryKeys.set(key, (inFlightTextDeliveryKeys.get(key) ?? 0) + 1);
|
|
875
|
-
}
|
|
876
|
-
};
|
|
877
|
-
const removeInFlightTextDelivery = (key) => {
|
|
878
|
-
if (!key)
|
|
879
|
-
return;
|
|
880
|
-
const remaining = (inFlightTextDeliveryKeys.get(key) ?? 1) - 1;
|
|
881
|
-
if (remaining > 0)
|
|
882
|
-
inFlightTextDeliveryKeys.set(key, remaining);
|
|
883
|
-
else
|
|
884
|
-
inFlightTextDeliveryKeys.delete(key);
|
|
885
|
-
};
|
|
886
533
|
return {
|
|
887
|
-
async execute(
|
|
888
|
-
if (!
|
|
534
|
+
async execute(key, effect) {
|
|
535
|
+
if (!key) {
|
|
889
536
|
latestInvocation += 1;
|
|
890
|
-
|
|
537
|
+
lastSuccessful = null;
|
|
891
538
|
return await effect();
|
|
892
539
|
}
|
|
893
|
-
|
|
540
|
+
const pendingDuplicate = inFlight.get(key);
|
|
541
|
+
if (pendingDuplicate) {
|
|
894
542
|
throw new MessageChannelDuplicateActionError("in-flight");
|
|
895
543
|
}
|
|
896
|
-
if (
|
|
544
|
+
if (lastSuccessful?.key === key) {
|
|
897
545
|
throw new MessageChannelDuplicateActionError("completed");
|
|
898
546
|
}
|
|
899
547
|
const invocation = ++latestInvocation;
|
|
900
|
-
|
|
548
|
+
lastSuccessful = null;
|
|
901
549
|
const pending = Promise.resolve().then(effect);
|
|
902
|
-
|
|
903
|
-
addInFlightTextDelivery(textDeliveryKey);
|
|
550
|
+
inFlight.set(key, pending);
|
|
904
551
|
try {
|
|
905
552
|
const result = await pending;
|
|
906
|
-
if (!isErrorResult(result)) {
|
|
907
|
-
|
|
908
|
-
if (textDeliveryKey)
|
|
909
|
-
successfulTextDeliveryKeys.add(textDeliveryKey);
|
|
910
|
-
if (invocation === latestInvocation) {
|
|
911
|
-
lastSuccessfulActionKey = actionKey;
|
|
912
|
-
}
|
|
553
|
+
if (invocation === latestInvocation && !isErrorResult(result)) {
|
|
554
|
+
lastSuccessful = { key };
|
|
913
555
|
}
|
|
914
556
|
return result;
|
|
915
557
|
} finally {
|
|
916
|
-
if (
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
removeInFlightTextDelivery(textDeliveryKey);
|
|
558
|
+
if (inFlight.get(key) === pending)
|
|
559
|
+
inFlight.delete(key);
|
|
920
560
|
}
|
|
921
|
-
},
|
|
922
|
-
async executeRelay(actionKey, textDeliveryKey, effect) {
|
|
923
|
-
if (!actionKey)
|
|
924
|
-
return await effect();
|
|
925
|
-
if (inFlightActionKeys.has(actionKey) || textDeliveryKey && inFlightTextDeliveryKeys.has(textDeliveryKey)) {
|
|
926
|
-
throw new MessageChannelDuplicateActionError("in-flight");
|
|
927
|
-
}
|
|
928
|
-
if (successfulActionKeys.has(actionKey) || textDeliveryKey && successfulTextDeliveryKeys.has(textDeliveryKey)) {
|
|
929
|
-
throw new MessageChannelDuplicateActionError("completed");
|
|
930
|
-
}
|
|
931
|
-
const pending = Promise.resolve().then(effect);
|
|
932
|
-
inFlightActionKeys.set(actionKey, pending);
|
|
933
|
-
addInFlightTextDelivery(textDeliveryKey);
|
|
934
|
-
if (textDeliveryKey)
|
|
935
|
-
inFlightRelayTextDeliveryKeys.add(textDeliveryKey);
|
|
936
|
-
try {
|
|
937
|
-
const result = await pending;
|
|
938
|
-
if (!isErrorResult(result)) {
|
|
939
|
-
successfulActionKeys.add(actionKey);
|
|
940
|
-
if (textDeliveryKey) {
|
|
941
|
-
successfulTextDeliveryKeys.add(textDeliveryKey);
|
|
942
|
-
successfulRelayTextDeliveryKeys.add(textDeliveryKey);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return result;
|
|
946
|
-
} finally {
|
|
947
|
-
if (inFlightActionKeys.get(actionKey) === pending) {
|
|
948
|
-
inFlightActionKeys.delete(actionKey);
|
|
949
|
-
}
|
|
950
|
-
removeInFlightTextDelivery(textDeliveryKey);
|
|
951
|
-
if (textDeliveryKey)
|
|
952
|
-
inFlightRelayTextDeliveryKeys.delete(textDeliveryKey);
|
|
953
|
-
}
|
|
954
|
-
},
|
|
955
|
-
snapshot() {
|
|
956
|
-
if (inFlightActionKeys.size > 0 || inFlightTextDeliveryKeys.size > 0) {
|
|
957
|
-
return null;
|
|
958
|
-
}
|
|
959
|
-
return {
|
|
960
|
-
successfulActionKeys: [...successfulActionKeys],
|
|
961
|
-
successfulTextDeliveryKeys: [...successfulTextDeliveryKeys],
|
|
962
|
-
successfulRelayTextDeliveryKeys: [...successfulRelayTextDeliveryKeys],
|
|
963
|
-
lastSuccessfulActionKey
|
|
964
|
-
};
|
|
965
561
|
}
|
|
966
562
|
};
|
|
967
563
|
}
|
|
@@ -1748,40 +1344,50 @@ function createChannelTurnProgressBuilder(options = {}) {
|
|
|
1748
1344
|
}
|
|
1749
1345
|
|
|
1750
1346
|
// src/channels/gateway-core.ts
|
|
1347
|
+
var MAX_ACCEPTED_CLIENT_MESSAGE_IDS = 2048;
|
|
1348
|
+
function runtimeKey(runtime) {
|
|
1349
|
+
return `${runtime.agent_id}:${runtime.conversation_id}`;
|
|
1350
|
+
}
|
|
1351
|
+
function hasAgentRuntime(value) {
|
|
1352
|
+
return !!value.runtime?.agent_id;
|
|
1353
|
+
}
|
|
1354
|
+
function channelTagsForSources(sources) {
|
|
1355
|
+
return [...new Set(sources.map((source) => `channel:${source.channel}`))];
|
|
1356
|
+
}
|
|
1357
|
+
function stopReasonFromDelta(message) {
|
|
1358
|
+
const delta = message.delta;
|
|
1359
|
+
return delta.message_type === "stop_reason" && "stop_reason" in delta && typeof delta.stop_reason === "string" ? delta.stop_reason : null;
|
|
1360
|
+
}
|
|
1361
|
+
function runIdFromDelta(message) {
|
|
1362
|
+
const runId = "run_id" in message.delta ? message.delta.run_id : undefined;
|
|
1363
|
+
return typeof runId === "string" && runId.length > 0 ? runId : undefined;
|
|
1364
|
+
}
|
|
1365
|
+
function lifecycleOutcome(stopReason) {
|
|
1366
|
+
if (stopReason === "cancelled")
|
|
1367
|
+
return "cancelled";
|
|
1368
|
+
if (stopReason === "end_turn" || stopReason === "tool_rule") {
|
|
1369
|
+
return "completed";
|
|
1370
|
+
}
|
|
1371
|
+
return "error";
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1751
1374
|
class ChannelGateway {
|
|
1752
1375
|
client;
|
|
1753
1376
|
hooks;
|
|
1754
1377
|
states = new Map;
|
|
1755
1378
|
disposers = [];
|
|
1756
|
-
runtimeToolScopes;
|
|
1757
1379
|
registrationQueue = Promise.resolve();
|
|
1758
1380
|
constructor(client, hooks) {
|
|
1759
1381
|
this.client = client;
|
|
1760
1382
|
this.hooks = hooks;
|
|
1761
|
-
this.
|
|
1762
|
-
|
|
1763
|
-
const state = hasAgentRuntime(request) ? this.states.get(gatewayRuntimeKey(request.runtime)) : undefined;
|
|
1383
|
+
this.disposers.push(client.onMessage((message) => this.handleMessage(message)), client.onExternalToolCall((request) => {
|
|
1384
|
+
const state = hasAgentRuntime(request) ? this.states.get(runtimeKey(request.runtime)) : undefined;
|
|
1764
1385
|
const active = state?.active;
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
throw new Error(`Unknown or stale external tool scope: ${request.scope_id}`);
|
|
1768
|
-
}
|
|
1769
|
-
const sources = scoped?.sources ?? active?.routingSources ?? state?.routedSources ?? [];
|
|
1770
|
-
const activeOwnsScope = Boolean(scoped && active?.toolScopes.some((scope) => scope.id === scoped.id));
|
|
1771
|
-
try {
|
|
1772
|
-
return await hooks.executeExternalTool(request, sources, activeOwnsScope || !request.scope_id && active ? active?.idempotencyScope : null);
|
|
1773
|
-
} catch (error) {
|
|
1774
|
-
if (!(error instanceof MessageChannelDuplicateActionError))
|
|
1775
|
-
throw error;
|
|
1776
|
-
return {
|
|
1777
|
-
content: [{ type: "text", text: error.message }],
|
|
1778
|
-
is_error: true
|
|
1779
|
-
};
|
|
1780
|
-
}
|
|
1386
|
+
const sources = active?.routingSources ?? state?.routedSources ?? [];
|
|
1387
|
+
return hooks.executeExternalTool(request, sources, active?.idempotencyScope ?? null);
|
|
1781
1388
|
}));
|
|
1782
1389
|
}
|
|
1783
1390
|
close() {
|
|
1784
|
-
this.runtimeToolScopes.close();
|
|
1785
1391
|
for (const dispose of this.disposers.splice(0))
|
|
1786
1392
|
dispose();
|
|
1787
1393
|
this.client.close();
|
|
@@ -1806,34 +1412,16 @@ class ChannelGateway {
|
|
|
1806
1412
|
const workAtSubmit = state.active || state.pendingSourcesByClientMessageId.size > 0;
|
|
1807
1413
|
state.pendingSourcesByClientMessageId.set(delivery.clientMessageId, {
|
|
1808
1414
|
sources: uniqueLifecycleSources(delivery.sources),
|
|
1809
|
-
disposition: "submitting"
|
|
1810
|
-
automaticRelay: false
|
|
1415
|
+
disposition: "submitting"
|
|
1811
1416
|
});
|
|
1812
1417
|
try {
|
|
1813
|
-
|
|
1418
|
+
await this.enqueueRegistration(async () => {
|
|
1814
1419
|
state.routedSources = uniqueRoutedSources([
|
|
1815
1420
|
...state.routedSources,
|
|
1816
1421
|
...delivery.sources
|
|
1817
1422
|
]);
|
|
1818
|
-
|
|
1819
|
-
hooks: this.hooks,
|
|
1820
|
-
runtime: delivery.runtime,
|
|
1821
|
-
sources: delivery.sources
|
|
1822
|
-
});
|
|
1823
|
-
const scope = this.runtimeToolScopes.create(delivery.runtime, delivery.sources, automaticRelay, tool);
|
|
1824
|
-
try {
|
|
1825
|
-
await this.performRuntimeRegistration(state, delivery, automaticRelay, this.runtimeToolScopes.registrationGroups(delivery.runtime, false));
|
|
1826
|
-
return scope;
|
|
1827
|
-
} catch (error) {
|
|
1828
|
-
this.runtimeToolScopes.release(scope);
|
|
1829
|
-
throw error;
|
|
1830
|
-
}
|
|
1423
|
+
await this.performRuntimeRegistration(state, delivery);
|
|
1831
1424
|
});
|
|
1832
|
-
const submitted = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
|
|
1833
|
-
if (submitted) {
|
|
1834
|
-
submitted.automaticRelay = toolScope.automaticRelay;
|
|
1835
|
-
submitted.toolScope = toolScope;
|
|
1836
|
-
}
|
|
1837
1425
|
const response = await this.client.submitInput({
|
|
1838
1426
|
runtime: delivery.runtime,
|
|
1839
1427
|
payload: {
|
|
@@ -1846,24 +1434,21 @@ class ChannelGateway {
|
|
|
1846
1434
|
}
|
|
1847
1435
|
],
|
|
1848
1436
|
image_failure_mode: "drop",
|
|
1849
|
-
client_preferences: {}
|
|
1850
|
-
external_tool_scope_ids: this.runtimeToolScopes.selection(toolScope)
|
|
1437
|
+
client_preferences: {}
|
|
1851
1438
|
}
|
|
1852
1439
|
});
|
|
1853
1440
|
if (!response.accepted) {
|
|
1854
1441
|
state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
|
|
1855
|
-
this.runtimeToolScopes.release(toolScope);
|
|
1856
|
-
this.finishToolScopesIfIdle(state);
|
|
1857
1442
|
if (workAtSubmit && !state.active)
|
|
1858
1443
|
this.finishRejectedDelivery(state, delivery);
|
|
1859
1444
|
return false;
|
|
1860
1445
|
}
|
|
1861
|
-
rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1446
|
+
this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1862
1447
|
for (const source of delivery.sources) {
|
|
1863
1448
|
this.enqueueHook(state, () => this.hooks.onLifecycle({ type: "queued", source }));
|
|
1864
1449
|
}
|
|
1865
1450
|
if (response.disposition === "started") {
|
|
1866
|
-
this.activateSources(state, delivery.clientMessageId, delivery.sources
|
|
1451
|
+
this.activateSources(state, delivery.clientMessageId, delivery.sources);
|
|
1867
1452
|
state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
|
|
1868
1453
|
} else if (response.disposition === "queued") {
|
|
1869
1454
|
const pending = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
|
|
@@ -1874,19 +1459,20 @@ class ChannelGateway {
|
|
|
1874
1459
|
}
|
|
1875
1460
|
return true;
|
|
1876
1461
|
} catch (error) {
|
|
1877
|
-
const failed = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
|
|
1878
1462
|
state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
|
|
1879
|
-
if (failed?.toolScope) {
|
|
1880
|
-
this.runtimeToolScopes.release(failed.toolScope);
|
|
1881
|
-
}
|
|
1882
|
-
this.finishToolScopesIfIdle(state);
|
|
1883
1463
|
if (workAtSubmit && !state.active)
|
|
1884
1464
|
this.finishRejectedDelivery(state, delivery);
|
|
1885
1465
|
throw error;
|
|
1886
1466
|
}
|
|
1887
1467
|
}
|
|
1468
|
+
remainingSources(state) {
|
|
1469
|
+
return uniqueLifecycleSources([
|
|
1470
|
+
...state.active?.lifecycleSources ?? [],
|
|
1471
|
+
...Array.from(state.pendingSourcesByClientMessageId.values()).flatMap((pending) => pending.sources)
|
|
1472
|
+
]);
|
|
1473
|
+
}
|
|
1888
1474
|
finishRejectedDelivery(state, delivery) {
|
|
1889
|
-
const remainingSources =
|
|
1475
|
+
const remainingSources = this.remainingSources(state);
|
|
1890
1476
|
this.enqueueHook(state, () => this.hooks.onLifecycle({
|
|
1891
1477
|
type: "finished",
|
|
1892
1478
|
batchId: `channel-${delivery.clientMessageId}`,
|
|
@@ -1911,14 +1497,13 @@ class ChannelGateway {
|
|
|
1911
1497
|
}
|
|
1912
1498
|
state.pendingSourcesByClientMessageId.set(delivery.clientMessageId, {
|
|
1913
1499
|
sources,
|
|
1914
|
-
disposition: "queued"
|
|
1915
|
-
automaticRelay: false
|
|
1500
|
+
disposition: "queued"
|
|
1916
1501
|
});
|
|
1917
1502
|
state.routedSources = uniqueRoutedSources([
|
|
1918
1503
|
...state.routedSources,
|
|
1919
1504
|
...delivery.sources
|
|
1920
1505
|
]);
|
|
1921
|
-
rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1506
|
+
this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1922
1507
|
}
|
|
1923
1508
|
async restoreRuntime(runtime, sources) {
|
|
1924
1509
|
const state = this.getState(runtime);
|
|
@@ -1931,10 +1516,7 @@ class ChannelGateway {
|
|
|
1931
1516
|
lifecycleSources: uniqueLifecycleSources(sources),
|
|
1932
1517
|
progress: createChannelTurnProgressBuilder(),
|
|
1933
1518
|
richDraft: null,
|
|
1934
|
-
|
|
1935
|
-
idempotencyScope: createMessageChannelIdempotencyScope(),
|
|
1936
|
-
relayEligible: false,
|
|
1937
|
-
toolScopes: []
|
|
1519
|
+
idempotencyScope: createMessageChannelIdempotencyScope()
|
|
1938
1520
|
};
|
|
1939
1521
|
state.active = recoveredTurn;
|
|
1940
1522
|
}
|
|
@@ -1952,7 +1534,7 @@ class ChannelGateway {
|
|
|
1952
1534
|
return replayedRequestIds;
|
|
1953
1535
|
}
|
|
1954
1536
|
async adoptActiveDelivery(delivery) {
|
|
1955
|
-
const key =
|
|
1537
|
+
const key = runtimeKey(delivery.runtime);
|
|
1956
1538
|
const stateExisted = this.states.has(key);
|
|
1957
1539
|
const state = this.getState(delivery.runtime);
|
|
1958
1540
|
const batchId = `channel-${delivery.clientMessageId}`;
|
|
@@ -1961,8 +1543,8 @@ class ChannelGateway {
|
|
|
1961
1543
|
if (state.active.batchId !== batchId) {
|
|
1962
1544
|
throw new Error(`Cannot adopt ${batchId}; ${state.active.batchId} is already active`);
|
|
1963
1545
|
}
|
|
1964
|
-
this.activateSources(state, delivery.clientMessageId, delivery.sources
|
|
1965
|
-
rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1546
|
+
this.activateSources(state, delivery.clientMessageId, delivery.sources);
|
|
1547
|
+
this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1966
1548
|
return;
|
|
1967
1549
|
}
|
|
1968
1550
|
const previousRoutedSources = state.routedSources;
|
|
@@ -1974,21 +1556,19 @@ class ChannelGateway {
|
|
|
1974
1556
|
lifecycleSources: uniqueLifecycleSources(delivery.sources),
|
|
1975
1557
|
progress: createChannelTurnProgressBuilder(),
|
|
1976
1558
|
richDraft: null,
|
|
1977
|
-
|
|
1978
|
-
idempotencyScope: createMessageChannelIdempotencyScope(delivery.activeTurnState?.idempotency),
|
|
1979
|
-
relayEligible: delivery.activeTurnState?.automaticRelay ?? Boolean(delivery.activeTurnState),
|
|
1980
|
-
toolScopes: delivery.activeTurnState?.toolScopes ?? []
|
|
1559
|
+
idempotencyScope: createMessageChannelIdempotencyScope()
|
|
1981
1560
|
};
|
|
1982
1561
|
state.active = active;
|
|
1983
|
-
for (const scope of active.toolScopes)
|
|
1984
|
-
this.runtimeToolScopes.retain(scope);
|
|
1985
1562
|
state.routedSources = uniqueRoutedSources([
|
|
1986
1563
|
...state.routedSources,
|
|
1987
1564
|
...delivery.sources
|
|
1988
1565
|
]);
|
|
1989
|
-
rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1566
|
+
this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1990
1567
|
try {
|
|
1991
|
-
await this.performRuntimeRegistration(state, {
|
|
1568
|
+
await this.performRuntimeRegistration(state, {
|
|
1569
|
+
...delivery,
|
|
1570
|
+
content: ""
|
|
1571
|
+
});
|
|
1992
1572
|
if (state.active !== active)
|
|
1993
1573
|
return;
|
|
1994
1574
|
active.richDraft = this.hooks.createRichDraft?.({
|
|
@@ -2002,9 +1582,6 @@ class ChannelGateway {
|
|
|
2002
1582
|
}));
|
|
2003
1583
|
} catch (error) {
|
|
2004
1584
|
active.richDraft?.dispose();
|
|
2005
|
-
for (const scope of active.toolScopes) {
|
|
2006
|
-
this.runtimeToolScopes.release(scope);
|
|
2007
|
-
}
|
|
2008
1585
|
if (state.active === active)
|
|
2009
1586
|
state.active = null;
|
|
2010
1587
|
state.routedSources = previousRoutedSources;
|
|
@@ -2018,67 +1595,51 @@ class ChannelGateway {
|
|
|
2018
1595
|
});
|
|
2019
1596
|
}
|
|
2020
1597
|
releaseActiveDelivery(runtime, clientMessageId) {
|
|
2021
|
-
const key =
|
|
1598
|
+
const key = runtimeKey(runtime);
|
|
2022
1599
|
const state = this.states.get(key);
|
|
2023
1600
|
const active = state?.active;
|
|
2024
1601
|
if (!state || active?.batchId !== `channel-${clientMessageId}`) {
|
|
2025
|
-
return
|
|
1602
|
+
return false;
|
|
2026
1603
|
}
|
|
2027
|
-
if (state.hookQueue)
|
|
2028
|
-
return null;
|
|
2029
|
-
const idempotency = active.idempotencyScope.snapshot();
|
|
2030
|
-
if (!idempotency)
|
|
2031
|
-
return null;
|
|
2032
|
-
const toolScopes = active.toolScopes.map((scope) => this.runtimeToolScopes.detach(scope));
|
|
2033
|
-
const handoffState = {
|
|
2034
|
-
assistantText: active.assistantText.snapshot(),
|
|
2035
|
-
idempotency,
|
|
2036
|
-
automaticRelay: active.relayEligible,
|
|
2037
|
-
...toolScopes.length > 0 ? { toolScopes } : {}
|
|
2038
|
-
};
|
|
2039
1604
|
active.richDraft?.dispose();
|
|
2040
1605
|
this.states.delete(key);
|
|
2041
|
-
return
|
|
1606
|
+
return true;
|
|
2042
1607
|
}
|
|
2043
1608
|
async registerRuntime(runtime, sources = [], defaultPermissionMode) {
|
|
2044
1609
|
const state = this.getState(runtime);
|
|
2045
1610
|
await this.enqueueRegistration(async () => {
|
|
2046
1611
|
this.setRoutedSources(runtime, sources);
|
|
2047
|
-
const prepared = await prepareGatewayRegistrationPolicy({
|
|
2048
|
-
hooks: this.hooks,
|
|
2049
|
-
runtime,
|
|
2050
|
-
sources
|
|
2051
|
-
});
|
|
2052
|
-
const externalTools = prepared.tool ? [{ tools: [prepared.tool] }] : [];
|
|
2053
|
-
this.runtimeToolScopes.setDesiredUnscoped(runtime, externalTools);
|
|
2054
|
-
if (this.runtimeToolScopes.hasRetainedScopes(runtime))
|
|
2055
|
-
return;
|
|
2056
1612
|
await this.performRuntimeRegistration(state, {
|
|
2057
1613
|
runtime,
|
|
2058
1614
|
content: "",
|
|
2059
1615
|
sources,
|
|
2060
1616
|
clientMessageId: "recovered",
|
|
2061
1617
|
...defaultPermissionMode ? { defaultPermissionMode } : {}
|
|
2062
|
-
}
|
|
1618
|
+
});
|
|
2063
1619
|
});
|
|
2064
1620
|
}
|
|
2065
1621
|
async publishRuntimeTools(runtime, sources = []) {
|
|
2066
1622
|
return await this.enqueueRegistration(async () => {
|
|
2067
|
-
if (this.states.has(
|
|
1623
|
+
if (this.states.has(runtimeKey(runtime)))
|
|
2068
1624
|
return false;
|
|
2069
1625
|
const tool = await this.hooks.buildExternalTool(runtime, sources);
|
|
2070
|
-
await this.
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
1626
|
+
const response = await this.client.runtimeExternalToolsUpdate({
|
|
1627
|
+
updates: [
|
|
1628
|
+
{
|
|
1629
|
+
runtimes: [runtime],
|
|
1630
|
+
external_tools: tool ? [{ tools: [tool] }] : []
|
|
1631
|
+
}
|
|
1632
|
+
]
|
|
1633
|
+
});
|
|
1634
|
+
if (!response.success) {
|
|
1635
|
+
throw new Error(response.error ?? "Failed to publish channel runtime tools");
|
|
1636
|
+
}
|
|
2076
1637
|
return tool !== null;
|
|
2077
1638
|
});
|
|
2078
1639
|
}
|
|
2079
1640
|
async releaseRuntimeTools(runtime, routedSources = [], options = {}) {
|
|
2080
1641
|
await this.enqueueRegistration(async () => {
|
|
2081
|
-
const key =
|
|
1642
|
+
const key = runtimeKey(runtime);
|
|
2082
1643
|
const state = this.states.get(key);
|
|
2083
1644
|
if (options.cleanupIdleRuntime) {
|
|
2084
1645
|
if (routedSources.length > 0 || (state?.routedSources.length ?? 0) > 0) {
|
|
@@ -2100,7 +1661,6 @@ class ChannelGateway {
|
|
|
2100
1661
|
if (!response.success) {
|
|
2101
1662
|
throw new Error(response.error ?? "Failed to release channel runtime tools");
|
|
2102
1663
|
}
|
|
2103
|
-
this.runtimeToolScopes.forgetRuntime(runtime);
|
|
2104
1664
|
});
|
|
2105
1665
|
}
|
|
2106
1666
|
async submitApprovalResponse(runtime, response) {
|
|
@@ -2118,14 +1678,21 @@ class ChannelGateway {
|
|
|
2118
1678
|
}
|
|
2119
1679
|
updateRoutedRuntimeTools(updates, routedSources) {
|
|
2120
1680
|
return this.enqueueRegistration(async () => {
|
|
2121
|
-
|
|
1681
|
+
if (updates.length > 0) {
|
|
1682
|
+
const response = await this.client.runtimeExternalToolsUpdate({
|
|
1683
|
+
updates
|
|
1684
|
+
});
|
|
1685
|
+
if (!response.success) {
|
|
1686
|
+
throw new Error(response.error ?? "Failed to update routed runtime tools");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
2122
1689
|
for (const update of routedSources) {
|
|
2123
1690
|
this.setRoutedSources(update.runtime, update.sources);
|
|
2124
1691
|
}
|
|
2125
1692
|
});
|
|
2126
1693
|
}
|
|
2127
1694
|
getModelStatus(runtime) {
|
|
2128
|
-
return this.states.get(
|
|
1695
|
+
return this.states.get(runtimeKey(runtime))?.modelStatus ?? null;
|
|
2129
1696
|
}
|
|
2130
1697
|
updateModelStatus(runtime, modelHandle) {
|
|
2131
1698
|
const state = this.getState(runtime);
|
|
@@ -2135,7 +1702,7 @@ class ChannelGateway {
|
|
|
2135
1702
|
};
|
|
2136
1703
|
}
|
|
2137
1704
|
getState(runtime) {
|
|
2138
|
-
const key =
|
|
1705
|
+
const key = runtimeKey(runtime);
|
|
2139
1706
|
let state = this.states.get(key);
|
|
2140
1707
|
if (!state) {
|
|
2141
1708
|
state = {
|
|
@@ -2155,10 +1722,14 @@ class ChannelGateway {
|
|
|
2155
1722
|
}
|
|
2156
1723
|
return state;
|
|
2157
1724
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
1725
|
+
rememberAcceptedClientMessageId(state, clientMessageId) {
|
|
1726
|
+
state.acceptedClientMessageIds.delete(clientMessageId);
|
|
1727
|
+
state.acceptedClientMessageIds.add(clientMessageId);
|
|
1728
|
+
if (state.acceptedClientMessageIds.size <= MAX_ACCEPTED_CLIENT_MESSAGE_IDS)
|
|
2160
1729
|
return;
|
|
2161
|
-
|
|
1730
|
+
const oldest = state.acceptedClientMessageIds.values().next().value;
|
|
1731
|
+
if (oldest)
|
|
1732
|
+
state.acceptedClientMessageIds.delete(oldest);
|
|
2162
1733
|
}
|
|
2163
1734
|
enqueueHook(state, hook) {
|
|
2164
1735
|
let pending;
|
|
@@ -2192,15 +1763,52 @@ class ChannelGateway {
|
|
|
2192
1763
|
});
|
|
2193
1764
|
return result;
|
|
2194
1765
|
}
|
|
2195
|
-
async performRuntimeRegistration(state, delivery
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
...externalToolsOverride === undefined ? {} : { externalToolsOverride }
|
|
1766
|
+
async performRuntimeRegistration(state, delivery) {
|
|
1767
|
+
const tool = await this.hooks.buildExternalTool(delivery.runtime, delivery.sources);
|
|
1768
|
+
const conversationTags = channelTagsForSources(delivery.sources);
|
|
1769
|
+
const signature = JSON.stringify({
|
|
1770
|
+
mode: delivery.defaultPermissionMode ?? null,
|
|
1771
|
+
tool,
|
|
1772
|
+
conversationTags
|
|
2203
1773
|
});
|
|
1774
|
+
if (state.registrationSignature === signature && state.registration) {
|
|
1775
|
+
return state.registration;
|
|
1776
|
+
}
|
|
1777
|
+
const registration = this.client.runtimeStart({
|
|
1778
|
+
agent_id: delivery.runtime.agent_id,
|
|
1779
|
+
conversation_id: delivery.runtime.conversation_id,
|
|
1780
|
+
...conversationTags.length > 0 ? { conversation_source_tags: conversationTags } : {},
|
|
1781
|
+
...delivery.defaultPermissionMode ? { mode: delivery.defaultPermissionMode } : {},
|
|
1782
|
+
recover_approvals: true,
|
|
1783
|
+
force_device_status: false,
|
|
1784
|
+
wait_for_replay: true,
|
|
1785
|
+
preserve_skill_sources: true,
|
|
1786
|
+
client_info: { name: "channel-gateway", title: "Channel Gateway" },
|
|
1787
|
+
external_tools: tool ? [{ tools: [tool] }] : []
|
|
1788
|
+
}).then((response) => {
|
|
1789
|
+
if (!response.success) {
|
|
1790
|
+
throw new Error(response.error ?? "Failed to register channel runtime");
|
|
1791
|
+
}
|
|
1792
|
+
const agentRecord = response.agent;
|
|
1793
|
+
const conversationRecord = response.conversation;
|
|
1794
|
+
const agentModel = typeof agentRecord?.model === "string" ? agentRecord.model : response.agent?.llm_config?.model ?? null;
|
|
1795
|
+
const conversationModel = typeof conversationRecord?.model === "string" ? conversationRecord.model : null;
|
|
1796
|
+
state.modelStatus = {
|
|
1797
|
+
modelHandle: delivery.runtime.conversation_id === "default" ? agentModel : conversationModel ?? agentModel,
|
|
1798
|
+
scope: delivery.runtime.conversation_id === "default" ? "agent" : "conversation"
|
|
1799
|
+
};
|
|
1800
|
+
});
|
|
1801
|
+
state.registrationSignature = signature;
|
|
1802
|
+
state.registration = registration;
|
|
1803
|
+
try {
|
|
1804
|
+
await registration;
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
if (state.registration === registration) {
|
|
1807
|
+
state.registration = null;
|
|
1808
|
+
state.registrationSignature = null;
|
|
1809
|
+
}
|
|
1810
|
+
throw error;
|
|
1811
|
+
}
|
|
2204
1812
|
}
|
|
2205
1813
|
handleMessage(message) {
|
|
2206
1814
|
if (message.type === "update_queue") {
|
|
@@ -2248,25 +1856,15 @@ class ChannelGateway {
|
|
|
2248
1856
|
continue;
|
|
2249
1857
|
}
|
|
2250
1858
|
const target = pending.removalDisposition === "dequeued" ? dequeued : cancelled;
|
|
2251
|
-
target.push({
|
|
2252
|
-
clientMessageId,
|
|
2253
|
-
sources: pending.sources,
|
|
2254
|
-
automaticRelay: pending.automaticRelay,
|
|
2255
|
-
...pending.toolScope ? { toolScope: pending.toolScope } : {}
|
|
2256
|
-
});
|
|
1859
|
+
target.push({ clientMessageId, sources: pending.sources });
|
|
2257
1860
|
state.pendingSourcesByClientMessageId.delete(clientMessageId);
|
|
2258
1861
|
}
|
|
2259
1862
|
const firstDequeued = dequeued[0];
|
|
2260
1863
|
if (firstDequeued) {
|
|
2261
|
-
|
|
2262
|
-
this.activateSources(state, firstDequeued.clientMessageId, dequeued.flatMap((entry) => entry.sources), dequeued.every((entry) => entry.automaticRelay), dequeuedScopes[0]);
|
|
2263
|
-
if (state.active)
|
|
2264
|
-
state.active.toolScopes.push(...dequeuedScopes.slice(1));
|
|
1864
|
+
this.activateSources(state, firstDequeued.clientMessageId, dequeued.flatMap((entry) => entry.sources));
|
|
2265
1865
|
}
|
|
2266
1866
|
for (const entry of cancelled) {
|
|
2267
|
-
|
|
2268
|
-
this.runtimeToolScopes.release(entry.toolScope);
|
|
2269
|
-
const remainingSources = remainingGatewaySources(state);
|
|
1867
|
+
const remainingSources = this.remainingSources(state);
|
|
2270
1868
|
this.enqueueHook(state, () => this.hooks.onLifecycle({
|
|
2271
1869
|
type: "finished",
|
|
2272
1870
|
batchId: `channel-${entry.clientMessageId}`,
|
|
@@ -2276,12 +1874,9 @@ class ChannelGateway {
|
|
|
2276
1874
|
...remainingSources.length ? { remainingSources } : {}
|
|
2277
1875
|
}));
|
|
2278
1876
|
}
|
|
2279
|
-
this.finishToolScopesIfIdle(state);
|
|
2280
1877
|
}
|
|
2281
|
-
activateSources(state, clientMessageId, sources
|
|
1878
|
+
activateSources(state, clientMessageId, sources) {
|
|
2282
1879
|
if (state.active) {
|
|
2283
|
-
if (toolScope)
|
|
2284
|
-
state.active.toolScopes.push(toolScope);
|
|
2285
1880
|
const knownLifecycleKeys = new Set(state.active.lifecycleSources.map(sourceLifecycleKey));
|
|
2286
1881
|
const addedLifecycleSources = uniqueLifecycleSources(sources).filter((source) => !knownLifecycleKeys.has(sourceLifecycleKey(source)));
|
|
2287
1882
|
if (addedLifecycleSources.length === 0)
|
|
@@ -2294,7 +1889,6 @@ class ChannelGateway {
|
|
|
2294
1889
|
...state.active.routingSources,
|
|
2295
1890
|
...sources
|
|
2296
1891
|
]);
|
|
2297
|
-
state.active.relayEligible &&= automaticRelay;
|
|
2298
1892
|
const processingEvent2 = {
|
|
2299
1893
|
type: "processing",
|
|
2300
1894
|
batchId: state.active.batchId,
|
|
@@ -2314,10 +1908,7 @@ class ChannelGateway {
|
|
|
2314
1908
|
batchId: `channel-${clientMessageId}`,
|
|
2315
1909
|
sources: routingSources
|
|
2316
1910
|
}) ?? null,
|
|
2317
|
-
|
|
2318
|
-
idempotencyScope: createMessageChannelIdempotencyScope(),
|
|
2319
|
-
relayEligible: automaticRelay,
|
|
2320
|
-
toolScopes: toolScope ? [toolScope] : []
|
|
1911
|
+
idempotencyScope: createMessageChannelIdempotencyScope()
|
|
2321
1912
|
};
|
|
2322
1913
|
const processingEvent = {
|
|
2323
1914
|
type: "processing",
|
|
@@ -2336,7 +1927,6 @@ class ChannelGateway {
|
|
|
2336
1927
|
const runId = runIdFromDelta(message);
|
|
2337
1928
|
if (runId)
|
|
2338
1929
|
active.runId = runId;
|
|
2339
|
-
this.enqueueFinalizedAssistantMessages(state, active, active.assistantText.handleDelta(message));
|
|
2340
1930
|
for (const update of active.progress.buildUpdates(message.delta)) {
|
|
2341
1931
|
this.enqueueHook(state, () => this.hooks.onProgress({
|
|
2342
1932
|
type: "progress",
|
|
@@ -2347,46 +1937,23 @@ class ChannelGateway {
|
|
|
2347
1937
|
}
|
|
2348
1938
|
active.richDraft?.handleDelta(message.delta);
|
|
2349
1939
|
const stopReason = stopReasonFromDelta(message);
|
|
2350
|
-
if (stopReason === "requires_approval" || stopReason === "end_turn"
|
|
1940
|
+
if (stopReason === "requires_approval" || stopReason === "end_turn") {
|
|
2351
1941
|
active.richDraft?.flushPending();
|
|
2352
|
-
const finalized = active.assistantText.finalizeCurrent();
|
|
2353
|
-
if (finalized) {
|
|
2354
|
-
this.enqueueFinalizedAssistantMessages(state, active, [finalized]);
|
|
2355
|
-
}
|
|
2356
1942
|
}
|
|
2357
1943
|
}
|
|
2358
|
-
enqueueFinalizedAssistantMessages(state, active, messages) {
|
|
2359
|
-
const relay = active.relayEligible ? this.hooks.relayAssistantText : undefined;
|
|
2360
|
-
for (const message of messages)
|
|
2361
|
-
this.enqueueHook(state, () => relayFinalizedAssistantMessage({
|
|
2362
|
-
message,
|
|
2363
|
-
sources: active.routingSources,
|
|
2364
|
-
idempotencyScope: active.idempotencyScope,
|
|
2365
|
-
relay
|
|
2366
|
-
}));
|
|
2367
|
-
}
|
|
2368
1944
|
handleTurnFinished(runtime, terminal) {
|
|
2369
1945
|
const state = this.getState(runtime);
|
|
2370
1946
|
const active = state.active;
|
|
2371
1947
|
if (!active)
|
|
2372
1948
|
return;
|
|
2373
|
-
if (terminal.stopReason === "end_turn" || terminal.stopReason === "tool_rule") {
|
|
2374
|
-
const finalized = active.assistantText.finalizeCurrent();
|
|
2375
|
-
if (finalized) {
|
|
2376
|
-
this.enqueueFinalizedAssistantMessages(state, active, [finalized]);
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
1949
|
state.active = null;
|
|
2380
1950
|
active.richDraft?.dispose();
|
|
2381
|
-
|
|
2382
|
-
this.runtimeToolScopes.release(scope);
|
|
2383
|
-
this.finishToolScopesIfIdle(state);
|
|
2384
|
-
const remainingSources = channelTurnOutcome(terminal.stopReason) === "completed" ? remainingGatewaySources(state) : [];
|
|
1951
|
+
const remainingSources = lifecycleOutcome(terminal.stopReason) === "completed" ? this.remainingSources(state) : [];
|
|
2385
1952
|
this.enqueueHook(state, () => this.hooks.onLifecycle({
|
|
2386
1953
|
type: "finished",
|
|
2387
1954
|
batchId: active.batchId,
|
|
2388
1955
|
sources: active.lifecycleSources,
|
|
2389
|
-
outcome:
|
|
1956
|
+
outcome: lifecycleOutcome(terminal.stopReason),
|
|
2390
1957
|
stopReason: terminal.stopReason,
|
|
2391
1958
|
...remainingSources.length ? { remainingSources } : {},
|
|
2392
1959
|
...terminal.runId ?? active.runId ? { runId: terminal.runId ?? active.runId } : {},
|
|
@@ -2396,7 +1963,7 @@ class ChannelGateway {
|
|
|
2396
1963
|
handleControlRequest(message) {
|
|
2397
1964
|
if (!message.agent_id || !message.conversation_id)
|
|
2398
1965
|
return;
|
|
2399
|
-
const state = this.states.get(
|
|
1966
|
+
const state = this.states.get(runtimeKey({
|
|
2400
1967
|
agent_id: message.agent_id,
|
|
2401
1968
|
conversation_id: message.conversation_id
|
|
2402
1969
|
}));
|
|
@@ -3136,20 +2703,6 @@ function messageIdempotencyKey(request, route) {
|
|
|
3136
2703
|
replyToMessageId: effectiveTextReplyId(request, route)
|
|
3137
2704
|
});
|
|
3138
2705
|
}
|
|
3139
|
-
function textDeliveryFingerprint(request, route) {
|
|
3140
|
-
if (request.action !== "send" && request.action !== "send-rich" || request.mediaPath) {
|
|
3141
|
-
return null;
|
|
3142
|
-
}
|
|
3143
|
-
return JSON.stringify({
|
|
3144
|
-
channel: request.channel,
|
|
3145
|
-
chatId: route.chatId,
|
|
3146
|
-
accountId: route.accountId ?? null,
|
|
3147
|
-
chatType: route.chatType ?? null,
|
|
3148
|
-
threadId: effectiveTextThreadId(request, route),
|
|
3149
|
-
message: request.message?.trim() ?? null,
|
|
3150
|
-
replyToMessageId: effectiveTextReplyId(request, route)
|
|
3151
|
-
});
|
|
3152
|
-
}
|
|
3153
2706
|
async function executeMessageChannel(input, options) {
|
|
3154
2707
|
const normalized = normalizeMessageChannelInput(input, options.resolver);
|
|
3155
2708
|
if (typeof normalized === "string")
|
|
@@ -3218,7 +2771,7 @@ async function executeMessageChannel(input, options) {
|
|
|
3218
2771
|
});
|
|
3219
2772
|
const requestThreadId = normalized.action === "download-file" ? normalized.threadId : inferredThreadId ?? (normalized.channel === "telegram" && context2.route.chatType === "direct" ? normalized.threadId : context2.route.threadId ?? normalized.threadId);
|
|
3220
2773
|
const request2 = buildMessageChannelRequest(normalized, normalized.chatId, requestThreadId);
|
|
3221
|
-
return await dispatchWithIdempotency(request2, context2, options.idempotencyScope
|
|
2774
|
+
return await dispatchWithIdempotency(request2, context2, options.idempotencyScope);
|
|
3222
2775
|
}
|
|
3223
2776
|
if (normalized.channel !== "slack") {
|
|
3224
2777
|
return `Error: Explicit MessageChannel targets are not supported on ${normalized.channel}.`;
|
|
@@ -3251,13 +2804,10 @@ async function executeMessageChannel(input, options) {
|
|
|
3251
2804
|
async function executeMessageChannelExternalTool(input, options) {
|
|
3252
2805
|
return createMessageChannelExternalToolResult(await executeMessageChannel(input, options));
|
|
3253
2806
|
}
|
|
3254
|
-
function dispatchWithIdempotency(request, context, scope
|
|
2807
|
+
function dispatchWithIdempotency(request, context, scope) {
|
|
3255
2808
|
const dispatch = () => dispatchMessageChannelAction({ request, context });
|
|
3256
|
-
const
|
|
3257
|
-
|
|
3258
|
-
if (!scope)
|
|
3259
|
-
return dispatch();
|
|
3260
|
-
return mode === "relay" ? scope.executeRelay(actionKey, deliveryKey, dispatch) : scope.execute(actionKey, deliveryKey, dispatch);
|
|
2809
|
+
const key = messageIdempotencyKey(request, context.route);
|
|
2810
|
+
return scope ? scope.execute(key, dispatch) : dispatch();
|
|
3261
2811
|
}
|
|
3262
2812
|
// src/tools/descriptions/MessageChannel.md
|
|
3263
2813
|
var MessageChannel_default = '# MessageChannel\n\nSend a message or channel action to an external channel.\n\nWhen you receive a `<channel-notification>`, use this tool to reply directly to the user on the same external channel. A normal assistant response is not delivered back to the external channel automatically.\n\nThere are two supported send modes:\n- Reply mode: use `channel` + `chat_id` from the notification to respond in the current routed chat.\n- Proactive mode: use `channel` + `target` on supported channels to send to an explicit outbound destination.\n\nPreferred reply pattern:\n- `action="send"` to send a normal reply\n- `channel` + `chat_id` from the notification attributes\n- `message` for the text body\n\nParameters:\n- `action`: The action to perform. The exact available actions depend on the active channel plugins and are reflected in the JSON schema.\n- `channel`: The platform to send to.\n- `chat_id`: Reply target for the current routed chat. Use this when responding to a channel notification.\n- `target`: Explicit outbound target for proactive sends on supported channels.\n- `accountId`: Optional channel account selector when multiple eligible accounts are available.\n- `message`: Text body for `action="send"`.\n- `replyTo`: Optional message ID to reply to. Omit this unless you intentionally want the platform\'s quote/reply UI.\n- `messageId`: Optional target message id for message-scoped actions like reactions.\n- `emoji`: Optional reaction payload for channels that support reactions.\n- `remove`: Optional boolean to remove a reaction instead of adding it.\n- `media`: Optional absolute local file path for file/media uploads on channels that support uploads.\n- `filename`: Optional uploaded filename override when supported by the channel.\n- `title`: Optional uploaded attachment title when supported by the channel.\n\nRules:\n- Always pass `action` explicitly, even for a normal reply.\n- Pass exactly one of `chat_id` or `target`.\n- `react` should be its own call.\n- `upload-file` can include both `media` and `message` so the uploaded file has a caption/comment when the channel supports it.\n- Telegram supports `action="send-rich"` for Bot API Rich Messages from Markdown content. Use it for headings, lists, tables, rich block quotes, details blocks, formulas, and longer structured messages; use `upload-file` for local media files.\n\nTelegram rich messages:\n- In Telegram private chats, normal `action="send"` messages are sent through Bot API Rich Messages by default when the Telegram account enables `rich_private_chat_default`; with that setting disabled, use explicit `action="send-rich"` for rich delivery.\n- Use `action="send-rich"` with `channel="telegram"` for structured Markdown rendered by Telegram Bot API Rich Messages.\n- Use this when the output benefits from real headings, tables, block quotes, collapsible details, footnotes, task lists, or formulas.\n- The `message` field is passed as Telegram rich Markdown. Supported examples include:\n - headings: `# Heading`, `## Heading`\n - lists: `- item`, `1. item`, `- [ ] task`, `- [x] done`\n - tables: GitHub-style pipe tables\n - block quotes: `> quoted text`\n - collapsible details: `<details><summary>Title</summary>content</details>`\n - inline math: `$E = mc^2$`\n - display math: `$$E = mc^2$$` or fenced `math` code blocks\n - footnotes: `text[^id]` with `[^id]: definition`\n- Prefer dollar-delimited math. `\\(...\\)` and `\\[...\\]` do not reliably render as formulas in Telegram rich Markdown.\n- Do not use `send-rich` for local file uploads. Use `upload-file`; rich Markdown media blocks only support HTTP/HTTPS URLs.\n- Rich messages persist only when the final `send-rich` call executes. If Telegram draft streaming is enabled for the account, the channel runtime may show ephemeral previews automatically; do not try to manage draft lifecycle from the tool call.\n';
|
|
@@ -3357,15 +2907,12 @@ function resolveMessageChannelToolChannels(channels) {
|
|
|
3357
2907
|
actions.add(action);
|
|
3358
2908
|
schemaContributions.push(...asSchemaContributionArray(discovery?.schema));
|
|
3359
2909
|
}
|
|
3360
|
-
const routedDestinationKeys = new Set(channels.map((channel) => channel.routedDestinationKey).filter(Boolean));
|
|
3361
|
-
const hasCompleteDestinationKeys = channels.every((channel) => channel.routedDestinationKey);
|
|
3362
2910
|
return {
|
|
3363
2911
|
activeChannels: [...uniqueChannels.keys()],
|
|
3364
2912
|
displayNames: [...uniqueChannels.values()].map((channel) => channel.displayName),
|
|
3365
2913
|
accountIds: [...accountIds],
|
|
3366
2914
|
actions: [...actions],
|
|
3367
|
-
schemaContributions
|
|
3368
|
-
automaticRelay: channels.length > 0 && channels.every((channel) => channel.replyMode === "relay") && (hasCompleteDestinationKeys ? routedDestinationKeys.size === 1 : channels.length === 1)
|
|
2915
|
+
schemaContributions
|
|
3369
2916
|
};
|
|
3370
2917
|
}
|
|
3371
2918
|
function mergeSchemaContributions(schema, contributions) {
|
|
@@ -3443,9 +2990,7 @@ No external channel adapters are currently running.`;
|
|
|
3443
2990
|
const channelList = discovery.displayNames.join(", ");
|
|
3444
2991
|
const actionList = discovery.actions.join(", ");
|
|
3445
2992
|
const hasAction = (action) => discovery.actions.includes(action);
|
|
3446
|
-
const scopedReplyContract = scoped ?
|
|
3447
|
-
|
|
3448
|
-
This routed external channel turn uses automatic relay. Plain assistant text is delivered to the external user as each assistant message is finalized, so write the user-visible reply normally and do not make a duplicate MessageChannel send. When a host exposes MessageChannel alongside relay, use it only for explicit actions such as reactions and files; exact duplicate final text is suppressed.` : '\n\nThis tool is currently scoped to a routed external channel turn. Plain assistant text is not delivered to that external user. If a user-visible reply is appropriate, your final action for the turn must be one MessageChannel call with action="send", channel from the notification, chat_id from the notification, and message containing the reply. After that final send succeeds, do not repeat or paraphrase the sent message in assistant text; finish with only `Sent.` as the internal confirmation. This does not apply to a short acknowledgement sent before continuing substantive work. If no user-visible response is appropriate, do not call MessageChannel and do not send an empty acknowledgement. For lightweight acknowledgement, prefer action="react" when supported. If the useful response belongs later, schedule the follow-up instead of sending a placeholder.' : "";
|
|
2993
|
+
const scopedReplyContract = scoped ? '\n\nThis tool is currently scoped to a routed external channel turn. Plain assistant text is not delivered to that external user. If a user-visible reply is appropriate, your final action for the turn must be one MessageChannel call with action="send", channel from the notification, chat_id from the notification, and message containing the reply. After that final send succeeds, do not repeat or paraphrase the sent message in assistant text; finish with only `Sent.` as the internal confirmation. This does not apply to a short acknowledgement sent before continuing substantive work. If no user-visible response is appropriate, do not call MessageChannel and do not send an empty acknowledgement. For lightweight acknowledgement, prefer action="react" when supported. If the useful response belongs later, schedule the follow-up instead of sending a placeholder.' : "";
|
|
3449
2994
|
const slackWorkAcknowledgement = discovery.activeChannels.includes("slack") ? `
|
|
3450
2995
|
|
|
3451
2996
|
For Slack requests that require nontrivial work or several tool calls, send one short MessageChannel call with action="send" before starting other tools. This gives the Slack user verbal acknowledgement and a View in web link. Do not do this for no-ops, reaction-only responses, or simple no-tool answers.` : "";
|
|
@@ -3516,4 +3061,4 @@ export {
|
|
|
3516
3061
|
ChannelControlRequestCoordinator
|
|
3517
3062
|
};
|
|
3518
3063
|
|
|
3519
|
-
//# debugId=
|
|
3064
|
+
//# debugId=B90B3E001C72BB3264756E2164756E21
|