@letta-ai/letta-code 0.34.1 → 0.34.2
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 +621 -166
- package/dist/gateway-core.js.map +13 -8
- 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/types/agent/approval-execution.d.ts.map +1 -1
- package/dist/types/agent/message.d.ts.map +1 -1
- package/dist/types/agent/subagents/names.d.ts +1 -1
- package/dist/types/agent/subagents/names.d.ts.map +1 -1
- package/dist/types/channels/gateway-assistant-relay.d.ts +37 -0
- package/dist/types/channels/gateway-assistant-relay.d.ts.map +1 -0
- package/dist/types/channels/gateway-core.d.ts +8 -50
- package/dist/types/channels/gateway-core.d.ts.map +1 -1
- package/dist/types/channels/gateway-registration-policy.d.ts +14 -0
- package/dist/types/channels/gateway-registration-policy.d.ts.map +1 -0
- package/dist/types/channels/gateway-runtime-registration.d.ts +12 -0
- package/dist/types/channels/gateway-runtime-registration.d.ts.map +1 -0
- package/dist/types/channels/gateway-runtime-state.d.ts +48 -0
- package/dist/types/channels/gateway-runtime-state.d.ts.map +1 -0
- package/dist/types/channels/gateway-runtime-tool-scopes.d.ts +26 -0
- package/dist/types/channels/gateway-runtime-tool-scopes.d.ts.map +1 -0
- package/dist/types/channels/gateway-sources.d.ts +1 -0
- package/dist/types/channels/gateway-sources.d.ts.map +1 -1
- package/dist/types/channels/gateway-types.d.ts +71 -0
- package/dist/types/channels/gateway-types.d.ts.map +1 -0
- package/dist/types/channels/message-channel-executor.d.ts +1 -0
- package/dist/types/channels/message-channel-executor.d.ts.map +1 -1
- package/dist/types/channels/message-channel-idempotency.d.ts +14 -5
- package/dist/types/channels/message-channel-idempotency.d.ts.map +1 -1
- package/dist/types/channels/message-channel-tool-definition.d.ts +4 -1
- package/dist/types/channels/message-channel-tool-definition.d.ts.map +1 -1
- package/dist/types/channels/plugin-types.d.ts +3 -2
- package/dist/types/channels/plugin-types.d.ts.map +1 -1
- package/dist/types/channels/types.d.ts +3 -1
- package/dist/types/channels/types.d.ts.map +1 -1
- package/dist/types/reminders/disk-space.d.ts +34 -0
- package/dist/types/reminders/disk-space.d.ts.map +1 -1
- package/dist/types/tools/impl/github-pull-request-watch.d.ts +11 -4
- package/dist/types/tools/impl/github-pull-request-watch.d.ts.map +1 -1
- package/dist/types/tools/impl/workflow.d.ts.map +1 -1
- package/dist/types/tools/manager.d.ts +2 -4
- package/dist/types/tools/manager.d.ts.map +1 -1
- package/dist/types/tools/tool-name-mapping.d.ts +5 -0
- package/dist/types/tools/tool-name-mapping.d.ts.map +1 -0
- package/dist/types/tools/toolset-catalog.d.ts +1 -1
- package/dist/types/types/background-process-protocol.d.ts +15 -0
- package/dist/types/types/background-process-protocol.d.ts.map +1 -1
- package/dist/types/types/channel-account-protocol.d.ts +14 -0
- package/dist/types/types/channel-account-protocol.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +5 -11
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/websocket/listener/background-process-snapshot.d.ts.map +1 -1
- package/dist/types/websocket/listener/queue.d.ts.map +1 -1
- package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
- package/dist/types/websocket/listener/types.d.ts +5 -0
- package/dist/types/websocket/listener/types.d.ts.map +1 -1
- package/letta.js +28880 -27909
- package/package.json +1 -1
- package/scripts/source-file-size-baseline.json +2 -2
- package/skills/context-doctor/SKILL.md +1 -1
package/dist/gateway-core.js
CHANGED
|
@@ -485,6 +485,117 @@ 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
|
+
|
|
488
599
|
// src/channels/gateway-sources.ts
|
|
489
600
|
function sourceRouteKey(source) {
|
|
490
601
|
return [
|
|
@@ -514,6 +625,229 @@ function uniqueRoutedSources(sources) {
|
|
|
514
625
|
function uniqueLifecycleSources(sources) {
|
|
515
626
|
return uniqueSourcesBy(sources, sourceLifecycleKey);
|
|
516
627
|
}
|
|
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
|
+
}
|
|
517
851
|
|
|
518
852
|
// src/channels/message-channel-idempotency.ts
|
|
519
853
|
class MessageChannelDuplicateActionError extends Error {
|
|
@@ -526,38 +860,108 @@ class MessageChannelDuplicateActionError extends Error {
|
|
|
526
860
|
function isErrorResult(result) {
|
|
527
861
|
return result.startsWith("Error:");
|
|
528
862
|
}
|
|
529
|
-
function createMessageChannelIdempotencyScope() {
|
|
530
|
-
const
|
|
531
|
-
|
|
863
|
+
function createMessageChannelIdempotencyScope(state) {
|
|
864
|
+
const inFlightActionKeys = new Map;
|
|
865
|
+
const inFlightTextDeliveryKeys = new Map;
|
|
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;
|
|
532
871
|
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
|
+
};
|
|
533
886
|
return {
|
|
534
|
-
async execute(
|
|
535
|
-
if (!
|
|
887
|
+
async execute(actionKey, textDeliveryKey, effect) {
|
|
888
|
+
if (!actionKey) {
|
|
536
889
|
latestInvocation += 1;
|
|
537
|
-
|
|
890
|
+
lastSuccessfulActionKey = null;
|
|
538
891
|
return await effect();
|
|
539
892
|
}
|
|
540
|
-
|
|
541
|
-
if (pendingDuplicate) {
|
|
893
|
+
if (inFlightActionKeys.has(actionKey) || textDeliveryKey && inFlightRelayTextDeliveryKeys.has(textDeliveryKey)) {
|
|
542
894
|
throw new MessageChannelDuplicateActionError("in-flight");
|
|
543
895
|
}
|
|
544
|
-
if (
|
|
896
|
+
if (lastSuccessfulActionKey === actionKey || textDeliveryKey && successfulRelayTextDeliveryKeys.has(textDeliveryKey)) {
|
|
545
897
|
throw new MessageChannelDuplicateActionError("completed");
|
|
546
898
|
}
|
|
547
899
|
const invocation = ++latestInvocation;
|
|
548
|
-
|
|
900
|
+
lastSuccessfulActionKey = null;
|
|
549
901
|
const pending = Promise.resolve().then(effect);
|
|
550
|
-
|
|
902
|
+
inFlightActionKeys.set(actionKey, pending);
|
|
903
|
+
addInFlightTextDelivery(textDeliveryKey);
|
|
551
904
|
try {
|
|
552
905
|
const result = await pending;
|
|
553
|
-
if (
|
|
554
|
-
|
|
906
|
+
if (!isErrorResult(result)) {
|
|
907
|
+
successfulActionKeys.add(actionKey);
|
|
908
|
+
if (textDeliveryKey)
|
|
909
|
+
successfulTextDeliveryKeys.add(textDeliveryKey);
|
|
910
|
+
if (invocation === latestInvocation) {
|
|
911
|
+
lastSuccessfulActionKey = actionKey;
|
|
912
|
+
}
|
|
555
913
|
}
|
|
556
914
|
return result;
|
|
557
915
|
} finally {
|
|
558
|
-
if (
|
|
559
|
-
|
|
916
|
+
if (inFlightActionKeys.get(actionKey) === pending) {
|
|
917
|
+
inFlightActionKeys.delete(actionKey);
|
|
918
|
+
}
|
|
919
|
+
removeInFlightTextDelivery(textDeliveryKey);
|
|
560
920
|
}
|
|
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
|
+
};
|
|
561
965
|
}
|
|
562
966
|
};
|
|
563
967
|
}
|
|
@@ -1344,50 +1748,40 @@ function createChannelTurnProgressBuilder(options = {}) {
|
|
|
1344
1748
|
}
|
|
1345
1749
|
|
|
1346
1750
|
// 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
|
-
|
|
1374
1751
|
class ChannelGateway {
|
|
1375
1752
|
client;
|
|
1376
1753
|
hooks;
|
|
1377
1754
|
states = new Map;
|
|
1378
1755
|
disposers = [];
|
|
1756
|
+
runtimeToolScopes;
|
|
1379
1757
|
registrationQueue = Promise.resolve();
|
|
1380
1758
|
constructor(client, hooks) {
|
|
1381
1759
|
this.client = client;
|
|
1382
1760
|
this.hooks = hooks;
|
|
1383
|
-
this.
|
|
1384
|
-
|
|
1761
|
+
this.runtimeToolScopes = new GatewayRuntimeToolScopes(client);
|
|
1762
|
+
this.disposers.push(client.onMessage((message) => this.handleMessage(message)), client.onExternalToolCall(async (request) => {
|
|
1763
|
+
const state = hasAgentRuntime(request) ? this.states.get(gatewayRuntimeKey(request.runtime)) : undefined;
|
|
1385
1764
|
const active = state?.active;
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1765
|
+
const scoped = hasAgentRuntime(request) && request.scope_id ? this.runtimeToolScopes.resolve(request.runtime, request.scope_id) : null;
|
|
1766
|
+
if (request.scope_id && !scoped) {
|
|
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
|
+
}
|
|
1388
1781
|
}));
|
|
1389
1782
|
}
|
|
1390
1783
|
close() {
|
|
1784
|
+
this.runtimeToolScopes.close();
|
|
1391
1785
|
for (const dispose of this.disposers.splice(0))
|
|
1392
1786
|
dispose();
|
|
1393
1787
|
this.client.close();
|
|
@@ -1412,16 +1806,34 @@ class ChannelGateway {
|
|
|
1412
1806
|
const workAtSubmit = state.active || state.pendingSourcesByClientMessageId.size > 0;
|
|
1413
1807
|
state.pendingSourcesByClientMessageId.set(delivery.clientMessageId, {
|
|
1414
1808
|
sources: uniqueLifecycleSources(delivery.sources),
|
|
1415
|
-
disposition: "submitting"
|
|
1809
|
+
disposition: "submitting",
|
|
1810
|
+
automaticRelay: false
|
|
1416
1811
|
});
|
|
1417
1812
|
try {
|
|
1418
|
-
await this.enqueueRegistration(async () => {
|
|
1813
|
+
const toolScope = await this.enqueueRegistration(async () => {
|
|
1419
1814
|
state.routedSources = uniqueRoutedSources([
|
|
1420
1815
|
...state.routedSources,
|
|
1421
1816
|
...delivery.sources
|
|
1422
1817
|
]);
|
|
1423
|
-
await
|
|
1818
|
+
const { automaticRelay, tool } = await prepareGatewayRegistrationPolicy({
|
|
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
|
+
}
|
|
1424
1831
|
});
|
|
1832
|
+
const submitted = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
|
|
1833
|
+
if (submitted) {
|
|
1834
|
+
submitted.automaticRelay = toolScope.automaticRelay;
|
|
1835
|
+
submitted.toolScope = toolScope;
|
|
1836
|
+
}
|
|
1425
1837
|
const response = await this.client.submitInput({
|
|
1426
1838
|
runtime: delivery.runtime,
|
|
1427
1839
|
payload: {
|
|
@@ -1434,21 +1846,24 @@ class ChannelGateway {
|
|
|
1434
1846
|
}
|
|
1435
1847
|
],
|
|
1436
1848
|
image_failure_mode: "drop",
|
|
1437
|
-
client_preferences: {}
|
|
1849
|
+
client_preferences: {},
|
|
1850
|
+
external_tool_scope_ids: this.runtimeToolScopes.selection(toolScope)
|
|
1438
1851
|
}
|
|
1439
1852
|
});
|
|
1440
1853
|
if (!response.accepted) {
|
|
1441
1854
|
state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
|
|
1855
|
+
this.runtimeToolScopes.release(toolScope);
|
|
1856
|
+
this.finishToolScopesIfIdle(state);
|
|
1442
1857
|
if (workAtSubmit && !state.active)
|
|
1443
1858
|
this.finishRejectedDelivery(state, delivery);
|
|
1444
1859
|
return false;
|
|
1445
1860
|
}
|
|
1446
|
-
|
|
1861
|
+
rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1447
1862
|
for (const source of delivery.sources) {
|
|
1448
1863
|
this.enqueueHook(state, () => this.hooks.onLifecycle({ type: "queued", source }));
|
|
1449
1864
|
}
|
|
1450
1865
|
if (response.disposition === "started") {
|
|
1451
|
-
this.activateSources(state, delivery.clientMessageId, delivery.sources);
|
|
1866
|
+
this.activateSources(state, delivery.clientMessageId, delivery.sources, toolScope.automaticRelay, toolScope);
|
|
1452
1867
|
state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
|
|
1453
1868
|
} else if (response.disposition === "queued") {
|
|
1454
1869
|
const pending = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
|
|
@@ -1459,20 +1874,19 @@ class ChannelGateway {
|
|
|
1459
1874
|
}
|
|
1460
1875
|
return true;
|
|
1461
1876
|
} catch (error) {
|
|
1877
|
+
const failed = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
|
|
1462
1878
|
state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
|
|
1879
|
+
if (failed?.toolScope) {
|
|
1880
|
+
this.runtimeToolScopes.release(failed.toolScope);
|
|
1881
|
+
}
|
|
1882
|
+
this.finishToolScopesIfIdle(state);
|
|
1463
1883
|
if (workAtSubmit && !state.active)
|
|
1464
1884
|
this.finishRejectedDelivery(state, delivery);
|
|
1465
1885
|
throw error;
|
|
1466
1886
|
}
|
|
1467
1887
|
}
|
|
1468
|
-
remainingSources(state) {
|
|
1469
|
-
return uniqueLifecycleSources([
|
|
1470
|
-
...state.active?.lifecycleSources ?? [],
|
|
1471
|
-
...Array.from(state.pendingSourcesByClientMessageId.values()).flatMap((pending) => pending.sources)
|
|
1472
|
-
]);
|
|
1473
|
-
}
|
|
1474
1888
|
finishRejectedDelivery(state, delivery) {
|
|
1475
|
-
const remainingSources =
|
|
1889
|
+
const remainingSources = remainingGatewaySources(state);
|
|
1476
1890
|
this.enqueueHook(state, () => this.hooks.onLifecycle({
|
|
1477
1891
|
type: "finished",
|
|
1478
1892
|
batchId: `channel-${delivery.clientMessageId}`,
|
|
@@ -1497,13 +1911,14 @@ class ChannelGateway {
|
|
|
1497
1911
|
}
|
|
1498
1912
|
state.pendingSourcesByClientMessageId.set(delivery.clientMessageId, {
|
|
1499
1913
|
sources,
|
|
1500
|
-
disposition: "queued"
|
|
1914
|
+
disposition: "queued",
|
|
1915
|
+
automaticRelay: false
|
|
1501
1916
|
});
|
|
1502
1917
|
state.routedSources = uniqueRoutedSources([
|
|
1503
1918
|
...state.routedSources,
|
|
1504
1919
|
...delivery.sources
|
|
1505
1920
|
]);
|
|
1506
|
-
|
|
1921
|
+
rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1507
1922
|
}
|
|
1508
1923
|
async restoreRuntime(runtime, sources) {
|
|
1509
1924
|
const state = this.getState(runtime);
|
|
@@ -1516,7 +1931,10 @@ class ChannelGateway {
|
|
|
1516
1931
|
lifecycleSources: uniqueLifecycleSources(sources),
|
|
1517
1932
|
progress: createChannelTurnProgressBuilder(),
|
|
1518
1933
|
richDraft: null,
|
|
1519
|
-
|
|
1934
|
+
assistantText: new GatewayAssistantTextAccumulator,
|
|
1935
|
+
idempotencyScope: createMessageChannelIdempotencyScope(),
|
|
1936
|
+
relayEligible: false,
|
|
1937
|
+
toolScopes: []
|
|
1520
1938
|
};
|
|
1521
1939
|
state.active = recoveredTurn;
|
|
1522
1940
|
}
|
|
@@ -1534,7 +1952,7 @@ class ChannelGateway {
|
|
|
1534
1952
|
return replayedRequestIds;
|
|
1535
1953
|
}
|
|
1536
1954
|
async adoptActiveDelivery(delivery) {
|
|
1537
|
-
const key =
|
|
1955
|
+
const key = gatewayRuntimeKey(delivery.runtime);
|
|
1538
1956
|
const stateExisted = this.states.has(key);
|
|
1539
1957
|
const state = this.getState(delivery.runtime);
|
|
1540
1958
|
const batchId = `channel-${delivery.clientMessageId}`;
|
|
@@ -1543,8 +1961,8 @@ class ChannelGateway {
|
|
|
1543
1961
|
if (state.active.batchId !== batchId) {
|
|
1544
1962
|
throw new Error(`Cannot adopt ${batchId}; ${state.active.batchId} is already active`);
|
|
1545
1963
|
}
|
|
1546
|
-
this.activateSources(state, delivery.clientMessageId, delivery.sources);
|
|
1547
|
-
|
|
1964
|
+
this.activateSources(state, delivery.clientMessageId, delivery.sources, state.active.relayEligible);
|
|
1965
|
+
rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1548
1966
|
return;
|
|
1549
1967
|
}
|
|
1550
1968
|
const previousRoutedSources = state.routedSources;
|
|
@@ -1556,19 +1974,21 @@ class ChannelGateway {
|
|
|
1556
1974
|
lifecycleSources: uniqueLifecycleSources(delivery.sources),
|
|
1557
1975
|
progress: createChannelTurnProgressBuilder(),
|
|
1558
1976
|
richDraft: null,
|
|
1559
|
-
|
|
1977
|
+
assistantText: new GatewayAssistantTextAccumulator(delivery.activeTurnState?.assistantText),
|
|
1978
|
+
idempotencyScope: createMessageChannelIdempotencyScope(delivery.activeTurnState?.idempotency),
|
|
1979
|
+
relayEligible: delivery.activeTurnState?.automaticRelay ?? Boolean(delivery.activeTurnState),
|
|
1980
|
+
toolScopes: delivery.activeTurnState?.toolScopes ?? []
|
|
1560
1981
|
};
|
|
1561
1982
|
state.active = active;
|
|
1983
|
+
for (const scope of active.toolScopes)
|
|
1984
|
+
this.runtimeToolScopes.retain(scope);
|
|
1562
1985
|
state.routedSources = uniqueRoutedSources([
|
|
1563
1986
|
...state.routedSources,
|
|
1564
1987
|
...delivery.sources
|
|
1565
1988
|
]);
|
|
1566
|
-
|
|
1989
|
+
rememberAcceptedClientMessageId(state, delivery.clientMessageId);
|
|
1567
1990
|
try {
|
|
1568
|
-
await this.performRuntimeRegistration(state, {
|
|
1569
|
-
...delivery,
|
|
1570
|
-
content: ""
|
|
1571
|
-
});
|
|
1991
|
+
await this.performRuntimeRegistration(state, { ...delivery, content: "" }, active.relayEligible, this.runtimeToolScopes.registrationGroups(delivery.runtime, false));
|
|
1572
1992
|
if (state.active !== active)
|
|
1573
1993
|
return;
|
|
1574
1994
|
active.richDraft = this.hooks.createRichDraft?.({
|
|
@@ -1582,6 +2002,9 @@ class ChannelGateway {
|
|
|
1582
2002
|
}));
|
|
1583
2003
|
} catch (error) {
|
|
1584
2004
|
active.richDraft?.dispose();
|
|
2005
|
+
for (const scope of active.toolScopes) {
|
|
2006
|
+
this.runtimeToolScopes.release(scope);
|
|
2007
|
+
}
|
|
1585
2008
|
if (state.active === active)
|
|
1586
2009
|
state.active = null;
|
|
1587
2010
|
state.routedSources = previousRoutedSources;
|
|
@@ -1595,51 +2018,67 @@ class ChannelGateway {
|
|
|
1595
2018
|
});
|
|
1596
2019
|
}
|
|
1597
2020
|
releaseActiveDelivery(runtime, clientMessageId) {
|
|
1598
|
-
const key =
|
|
2021
|
+
const key = gatewayRuntimeKey(runtime);
|
|
1599
2022
|
const state = this.states.get(key);
|
|
1600
2023
|
const active = state?.active;
|
|
1601
2024
|
if (!state || active?.batchId !== `channel-${clientMessageId}`) {
|
|
1602
|
-
return
|
|
2025
|
+
return null;
|
|
1603
2026
|
}
|
|
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
|
+
};
|
|
1604
2039
|
active.richDraft?.dispose();
|
|
1605
2040
|
this.states.delete(key);
|
|
1606
|
-
return
|
|
2041
|
+
return handoffState;
|
|
1607
2042
|
}
|
|
1608
2043
|
async registerRuntime(runtime, sources = [], defaultPermissionMode) {
|
|
1609
2044
|
const state = this.getState(runtime);
|
|
1610
2045
|
await this.enqueueRegistration(async () => {
|
|
1611
2046
|
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;
|
|
1612
2056
|
await this.performRuntimeRegistration(state, {
|
|
1613
2057
|
runtime,
|
|
1614
2058
|
content: "",
|
|
1615
2059
|
sources,
|
|
1616
2060
|
clientMessageId: "recovered",
|
|
1617
2061
|
...defaultPermissionMode ? { defaultPermissionMode } : {}
|
|
1618
|
-
});
|
|
2062
|
+
}, prepared.automaticRelay, externalTools);
|
|
1619
2063
|
});
|
|
1620
2064
|
}
|
|
1621
2065
|
async publishRuntimeTools(runtime, sources = []) {
|
|
1622
2066
|
return await this.enqueueRegistration(async () => {
|
|
1623
|
-
if (this.states.has(
|
|
2067
|
+
if (this.states.has(gatewayRuntimeKey(runtime)))
|
|
1624
2068
|
return false;
|
|
1625
2069
|
const tool = await this.hooks.buildExternalTool(runtime, sources);
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
]
|
|
1633
|
-
});
|
|
1634
|
-
if (!response.success) {
|
|
1635
|
-
throw new Error(response.error ?? "Failed to publish channel runtime tools");
|
|
1636
|
-
}
|
|
2070
|
+
await this.runtimeToolScopes.updateDesiredUnscoped([
|
|
2071
|
+
{
|
|
2072
|
+
runtimes: [runtime],
|
|
2073
|
+
external_tools: tool ? [{ tools: [tool] }] : []
|
|
2074
|
+
}
|
|
2075
|
+
], () => false);
|
|
1637
2076
|
return tool !== null;
|
|
1638
2077
|
});
|
|
1639
2078
|
}
|
|
1640
2079
|
async releaseRuntimeTools(runtime, routedSources = [], options = {}) {
|
|
1641
2080
|
await this.enqueueRegistration(async () => {
|
|
1642
|
-
const key =
|
|
2081
|
+
const key = gatewayRuntimeKey(runtime);
|
|
1643
2082
|
const state = this.states.get(key);
|
|
1644
2083
|
if (options.cleanupIdleRuntime) {
|
|
1645
2084
|
if (routedSources.length > 0 || (state?.routedSources.length ?? 0) > 0) {
|
|
@@ -1661,6 +2100,7 @@ class ChannelGateway {
|
|
|
1661
2100
|
if (!response.success) {
|
|
1662
2101
|
throw new Error(response.error ?? "Failed to release channel runtime tools");
|
|
1663
2102
|
}
|
|
2103
|
+
this.runtimeToolScopes.forgetRuntime(runtime);
|
|
1664
2104
|
});
|
|
1665
2105
|
}
|
|
1666
2106
|
async submitApprovalResponse(runtime, response) {
|
|
@@ -1678,21 +2118,14 @@ class ChannelGateway {
|
|
|
1678
2118
|
}
|
|
1679
2119
|
updateRoutedRuntimeTools(updates, routedSources) {
|
|
1680
2120
|
return this.enqueueRegistration(async () => {
|
|
1681
|
-
|
|
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
|
-
}
|
|
2121
|
+
await this.runtimeToolScopes.updateDesiredUnscoped(updates, (runtime) => hasGatewayRuntimeWork(this.states.get(gatewayRuntimeKey(runtime))));
|
|
1689
2122
|
for (const update of routedSources) {
|
|
1690
2123
|
this.setRoutedSources(update.runtime, update.sources);
|
|
1691
2124
|
}
|
|
1692
2125
|
});
|
|
1693
2126
|
}
|
|
1694
2127
|
getModelStatus(runtime) {
|
|
1695
|
-
return this.states.get(
|
|
2128
|
+
return this.states.get(gatewayRuntimeKey(runtime))?.modelStatus ?? null;
|
|
1696
2129
|
}
|
|
1697
2130
|
updateModelStatus(runtime, modelHandle) {
|
|
1698
2131
|
const state = this.getState(runtime);
|
|
@@ -1702,7 +2135,7 @@ class ChannelGateway {
|
|
|
1702
2135
|
};
|
|
1703
2136
|
}
|
|
1704
2137
|
getState(runtime) {
|
|
1705
|
-
const key =
|
|
2138
|
+
const key = gatewayRuntimeKey(runtime);
|
|
1706
2139
|
let state = this.states.get(key);
|
|
1707
2140
|
if (!state) {
|
|
1708
2141
|
state = {
|
|
@@ -1722,14 +2155,10 @@ class ChannelGateway {
|
|
|
1722
2155
|
}
|
|
1723
2156
|
return state;
|
|
1724
2157
|
}
|
|
1725
|
-
|
|
1726
|
-
state.
|
|
1727
|
-
state.acceptedClientMessageIds.add(clientMessageId);
|
|
1728
|
-
if (state.acceptedClientMessageIds.size <= MAX_ACCEPTED_CLIENT_MESSAGE_IDS)
|
|
2158
|
+
finishToolScopesIfIdle(state) {
|
|
2159
|
+
if (state.active || state.pendingSourcesByClientMessageId.size > 0)
|
|
1729
2160
|
return;
|
|
1730
|
-
|
|
1731
|
-
if (oldest)
|
|
1732
|
-
state.acceptedClientMessageIds.delete(oldest);
|
|
2161
|
+
this.runtimeToolScopes.flushWhenIdle(state.runtime, () => hasGatewayRuntimeWork(this.states.get(gatewayRuntimeKey(state.runtime))), (task) => this.enqueueRegistration(task));
|
|
1733
2162
|
}
|
|
1734
2163
|
enqueueHook(state, hook) {
|
|
1735
2164
|
let pending;
|
|
@@ -1763,52 +2192,15 @@ class ChannelGateway {
|
|
|
1763
2192
|
});
|
|
1764
2193
|
return result;
|
|
1765
2194
|
}
|
|
1766
|
-
async performRuntimeRegistration(state, delivery) {
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2195
|
+
async performRuntimeRegistration(state, delivery, policyOverride, externalToolsOverride) {
|
|
2196
|
+
return await performGatewayRuntimeRegistration({
|
|
2197
|
+
client: this.client,
|
|
2198
|
+
hooks: this.hooks,
|
|
2199
|
+
state,
|
|
2200
|
+
delivery,
|
|
2201
|
+
...policyOverride === undefined ? {} : { policyOverride },
|
|
2202
|
+
...externalToolsOverride === undefined ? {} : { externalToolsOverride }
|
|
1773
2203
|
});
|
|
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
|
-
}
|
|
1812
2204
|
}
|
|
1813
2205
|
handleMessage(message) {
|
|
1814
2206
|
if (message.type === "update_queue") {
|
|
@@ -1856,15 +2248,25 @@ class ChannelGateway {
|
|
|
1856
2248
|
continue;
|
|
1857
2249
|
}
|
|
1858
2250
|
const target = pending.removalDisposition === "dequeued" ? dequeued : cancelled;
|
|
1859
|
-
target.push({
|
|
2251
|
+
target.push({
|
|
2252
|
+
clientMessageId,
|
|
2253
|
+
sources: pending.sources,
|
|
2254
|
+
automaticRelay: pending.automaticRelay,
|
|
2255
|
+
...pending.toolScope ? { toolScope: pending.toolScope } : {}
|
|
2256
|
+
});
|
|
1860
2257
|
state.pendingSourcesByClientMessageId.delete(clientMessageId);
|
|
1861
2258
|
}
|
|
1862
2259
|
const firstDequeued = dequeued[0];
|
|
1863
2260
|
if (firstDequeued) {
|
|
1864
|
-
|
|
2261
|
+
const dequeuedScopes = dequeued.flatMap((entry) => entry.toolScope ? [entry.toolScope] : []);
|
|
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));
|
|
1865
2265
|
}
|
|
1866
2266
|
for (const entry of cancelled) {
|
|
1867
|
-
|
|
2267
|
+
if (entry.toolScope)
|
|
2268
|
+
this.runtimeToolScopes.release(entry.toolScope);
|
|
2269
|
+
const remainingSources = remainingGatewaySources(state);
|
|
1868
2270
|
this.enqueueHook(state, () => this.hooks.onLifecycle({
|
|
1869
2271
|
type: "finished",
|
|
1870
2272
|
batchId: `channel-${entry.clientMessageId}`,
|
|
@@ -1874,9 +2276,12 @@ class ChannelGateway {
|
|
|
1874
2276
|
...remainingSources.length ? { remainingSources } : {}
|
|
1875
2277
|
}));
|
|
1876
2278
|
}
|
|
2279
|
+
this.finishToolScopesIfIdle(state);
|
|
1877
2280
|
}
|
|
1878
|
-
activateSources(state, clientMessageId, sources) {
|
|
2281
|
+
activateSources(state, clientMessageId, sources, automaticRelay, toolScope) {
|
|
1879
2282
|
if (state.active) {
|
|
2283
|
+
if (toolScope)
|
|
2284
|
+
state.active.toolScopes.push(toolScope);
|
|
1880
2285
|
const knownLifecycleKeys = new Set(state.active.lifecycleSources.map(sourceLifecycleKey));
|
|
1881
2286
|
const addedLifecycleSources = uniqueLifecycleSources(sources).filter((source) => !knownLifecycleKeys.has(sourceLifecycleKey(source)));
|
|
1882
2287
|
if (addedLifecycleSources.length === 0)
|
|
@@ -1889,6 +2294,7 @@ class ChannelGateway {
|
|
|
1889
2294
|
...state.active.routingSources,
|
|
1890
2295
|
...sources
|
|
1891
2296
|
]);
|
|
2297
|
+
state.active.relayEligible &&= automaticRelay;
|
|
1892
2298
|
const processingEvent2 = {
|
|
1893
2299
|
type: "processing",
|
|
1894
2300
|
batchId: state.active.batchId,
|
|
@@ -1908,7 +2314,10 @@ class ChannelGateway {
|
|
|
1908
2314
|
batchId: `channel-${clientMessageId}`,
|
|
1909
2315
|
sources: routingSources
|
|
1910
2316
|
}) ?? null,
|
|
1911
|
-
|
|
2317
|
+
assistantText: new GatewayAssistantTextAccumulator,
|
|
2318
|
+
idempotencyScope: createMessageChannelIdempotencyScope(),
|
|
2319
|
+
relayEligible: automaticRelay,
|
|
2320
|
+
toolScopes: toolScope ? [toolScope] : []
|
|
1912
2321
|
};
|
|
1913
2322
|
const processingEvent = {
|
|
1914
2323
|
type: "processing",
|
|
@@ -1927,6 +2336,7 @@ class ChannelGateway {
|
|
|
1927
2336
|
const runId = runIdFromDelta(message);
|
|
1928
2337
|
if (runId)
|
|
1929
2338
|
active.runId = runId;
|
|
2339
|
+
this.enqueueFinalizedAssistantMessages(state, active, active.assistantText.handleDelta(message));
|
|
1930
2340
|
for (const update of active.progress.buildUpdates(message.delta)) {
|
|
1931
2341
|
this.enqueueHook(state, () => this.hooks.onProgress({
|
|
1932
2342
|
type: "progress",
|
|
@@ -1937,23 +2347,46 @@ class ChannelGateway {
|
|
|
1937
2347
|
}
|
|
1938
2348
|
active.richDraft?.handleDelta(message.delta);
|
|
1939
2349
|
const stopReason = stopReasonFromDelta(message);
|
|
1940
|
-
if (stopReason === "requires_approval" || stopReason === "end_turn") {
|
|
2350
|
+
if (stopReason === "requires_approval" || stopReason === "end_turn" || stopReason === "tool_rule") {
|
|
1941
2351
|
active.richDraft?.flushPending();
|
|
2352
|
+
const finalized = active.assistantText.finalizeCurrent();
|
|
2353
|
+
if (finalized) {
|
|
2354
|
+
this.enqueueFinalizedAssistantMessages(state, active, [finalized]);
|
|
2355
|
+
}
|
|
1942
2356
|
}
|
|
1943
2357
|
}
|
|
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
|
+
}
|
|
1944
2368
|
handleTurnFinished(runtime, terminal) {
|
|
1945
2369
|
const state = this.getState(runtime);
|
|
1946
2370
|
const active = state.active;
|
|
1947
2371
|
if (!active)
|
|
1948
2372
|
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
|
+
}
|
|
1949
2379
|
state.active = null;
|
|
1950
2380
|
active.richDraft?.dispose();
|
|
1951
|
-
|
|
2381
|
+
for (const scope of active.toolScopes)
|
|
2382
|
+
this.runtimeToolScopes.release(scope);
|
|
2383
|
+
this.finishToolScopesIfIdle(state);
|
|
2384
|
+
const remainingSources = channelTurnOutcome(terminal.stopReason) === "completed" ? remainingGatewaySources(state) : [];
|
|
1952
2385
|
this.enqueueHook(state, () => this.hooks.onLifecycle({
|
|
1953
2386
|
type: "finished",
|
|
1954
2387
|
batchId: active.batchId,
|
|
1955
2388
|
sources: active.lifecycleSources,
|
|
1956
|
-
outcome:
|
|
2389
|
+
outcome: channelTurnOutcome(terminal.stopReason),
|
|
1957
2390
|
stopReason: terminal.stopReason,
|
|
1958
2391
|
...remainingSources.length ? { remainingSources } : {},
|
|
1959
2392
|
...terminal.runId ?? active.runId ? { runId: terminal.runId ?? active.runId } : {},
|
|
@@ -1963,7 +2396,7 @@ class ChannelGateway {
|
|
|
1963
2396
|
handleControlRequest(message) {
|
|
1964
2397
|
if (!message.agent_id || !message.conversation_id)
|
|
1965
2398
|
return;
|
|
1966
|
-
const state = this.states.get(
|
|
2399
|
+
const state = this.states.get(gatewayRuntimeKey({
|
|
1967
2400
|
agent_id: message.agent_id,
|
|
1968
2401
|
conversation_id: message.conversation_id
|
|
1969
2402
|
}));
|
|
@@ -2703,6 +3136,20 @@ function messageIdempotencyKey(request, route) {
|
|
|
2703
3136
|
replyToMessageId: effectiveTextReplyId(request, route)
|
|
2704
3137
|
});
|
|
2705
3138
|
}
|
|
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
|
+
}
|
|
2706
3153
|
async function executeMessageChannel(input, options) {
|
|
2707
3154
|
const normalized = normalizeMessageChannelInput(input, options.resolver);
|
|
2708
3155
|
if (typeof normalized === "string")
|
|
@@ -2771,7 +3218,7 @@ async function executeMessageChannel(input, options) {
|
|
|
2771
3218
|
});
|
|
2772
3219
|
const requestThreadId = normalized.action === "download-file" ? normalized.threadId : inferredThreadId ?? (normalized.channel === "telegram" && context2.route.chatType === "direct" ? normalized.threadId : context2.route.threadId ?? normalized.threadId);
|
|
2773
3220
|
const request2 = buildMessageChannelRequest(normalized, normalized.chatId, requestThreadId);
|
|
2774
|
-
return await dispatchWithIdempotency(request2, context2, options.idempotencyScope);
|
|
3221
|
+
return await dispatchWithIdempotency(request2, context2, options.idempotencyScope, options.idempotencyMode);
|
|
2775
3222
|
}
|
|
2776
3223
|
if (normalized.channel !== "slack") {
|
|
2777
3224
|
return `Error: Explicit MessageChannel targets are not supported on ${normalized.channel}.`;
|
|
@@ -2804,10 +3251,13 @@ async function executeMessageChannel(input, options) {
|
|
|
2804
3251
|
async function executeMessageChannelExternalTool(input, options) {
|
|
2805
3252
|
return createMessageChannelExternalToolResult(await executeMessageChannel(input, options));
|
|
2806
3253
|
}
|
|
2807
|
-
function dispatchWithIdempotency(request, context, scope) {
|
|
3254
|
+
function dispatchWithIdempotency(request, context, scope, mode = "explicit") {
|
|
2808
3255
|
const dispatch = () => dispatchMessageChannelAction({ request, context });
|
|
2809
|
-
const
|
|
2810
|
-
|
|
3256
|
+
const actionKey = messageIdempotencyKey(request, context.route);
|
|
3257
|
+
const deliveryKey = textDeliveryFingerprint(request, context.route);
|
|
3258
|
+
if (!scope)
|
|
3259
|
+
return dispatch();
|
|
3260
|
+
return mode === "relay" ? scope.executeRelay(actionKey, deliveryKey, dispatch) : scope.execute(actionKey, deliveryKey, dispatch);
|
|
2811
3261
|
}
|
|
2812
3262
|
// src/tools/descriptions/MessageChannel.md
|
|
2813
3263
|
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';
|
|
@@ -2907,12 +3357,15 @@ function resolveMessageChannelToolChannels(channels) {
|
|
|
2907
3357
|
actions.add(action);
|
|
2908
3358
|
schemaContributions.push(...asSchemaContributionArray(discovery?.schema));
|
|
2909
3359
|
}
|
|
3360
|
+
const routedDestinationKeys = new Set(channels.map((channel) => channel.routedDestinationKey).filter(Boolean));
|
|
3361
|
+
const hasCompleteDestinationKeys = channels.every((channel) => channel.routedDestinationKey);
|
|
2910
3362
|
return {
|
|
2911
3363
|
activeChannels: [...uniqueChannels.keys()],
|
|
2912
3364
|
displayNames: [...uniqueChannels.values()].map((channel) => channel.displayName),
|
|
2913
3365
|
accountIds: [...accountIds],
|
|
2914
3366
|
actions: [...actions],
|
|
2915
|
-
schemaContributions
|
|
3367
|
+
schemaContributions,
|
|
3368
|
+
automaticRelay: channels.length > 0 && channels.every((channel) => channel.replyMode === "relay") && (hasCompleteDestinationKeys ? routedDestinationKeys.size === 1 : channels.length === 1)
|
|
2916
3369
|
};
|
|
2917
3370
|
}
|
|
2918
3371
|
function mergeSchemaContributions(schema, contributions) {
|
|
@@ -2990,7 +3443,9 @@ No external channel adapters are currently running.`;
|
|
|
2990
3443
|
const channelList = discovery.displayNames.join(", ");
|
|
2991
3444
|
const actionList = discovery.actions.join(", ");
|
|
2992
3445
|
const hasAction = (action) => discovery.actions.includes(action);
|
|
2993
|
-
const scopedReplyContract = scoped ?
|
|
3446
|
+
const scopedReplyContract = scoped ? discovery.automaticRelay ? `
|
|
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.' : "";
|
|
2994
3449
|
const slackWorkAcknowledgement = discovery.activeChannels.includes("slack") ? `
|
|
2995
3450
|
|
|
2996
3451
|
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.` : "";
|
|
@@ -3061,4 +3516,4 @@ export {
|
|
|
3061
3516
|
ChannelControlRequestCoordinator
|
|
3062
3517
|
};
|
|
3063
3518
|
|
|
3064
|
-
//# debugId=
|
|
3519
|
+
//# debugId=88EE0831E20B6BB164756E2164756E21
|