@cjhyy/code-shell-core 0.9.4 → 0.9.6
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/credentials/access.d.ts +9 -1
- package/dist/credentials/access.js +15 -4
- package/dist/credentials/store.d.ts +11 -0
- package/dist/credentials/store.js +44 -9
- package/dist/credentials/types.d.ts +4 -0
- package/dist/credentials/types.js +4 -0
- package/dist/credentials/use-credential-tool.js +12 -2
- package/dist/engine/engine-workspace-authority.js +10 -3
- package/dist/engine/engine.d.ts +3 -0
- package/dist/engine/engine.js +38 -6
- package/dist/engine/prompt-cache-diagnostics.js +10 -1
- package/dist/engine/run-goal.js +7 -3
- package/dist/engine/run-types.d.ts +16 -0
- package/dist/engine/run-workspace.js +5 -21
- package/dist/engine/turn-loop.d.ts +4 -4
- package/dist/engine/turn-loop.js +29 -9
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/links/index.d.ts +1 -0
- package/dist/links/index.js +1 -0
- package/dist/links/link-action-tool.d.ts +2 -1
- package/dist/links/link-action-tool.js +80 -44
- package/dist/links/status.d.ts +53 -0
- package/dist/links/status.js +175 -0
- package/dist/llm/prompt-cache.d.ts +35 -3
- package/dist/llm/prompt-cache.js +63 -3
- package/dist/llm/providers/openai.d.ts +3 -0
- package/dist/llm/providers/openai.js +57 -14
- package/dist/protocol/background-result-wakeup.d.ts +8 -1
- package/dist/protocol/background-result-wakeup.js +76 -38
- package/dist/protocol/chat-session-manager.d.ts +7 -1
- package/dist/protocol/chat-session-manager.js +44 -8
- package/dist/protocol/chat-session.d.ts +2 -0
- package/dist/protocol/chat-session.js +4 -1
- package/dist/protocol/server.d.ts +3 -0
- package/dist/protocol/server.js +148 -43
- package/dist/protocol/session-message-result.d.ts +12 -0
- package/dist/protocol/session-message-result.js +42 -0
- package/dist/protocol/session-message-workspace.d.ts +21 -0
- package/dist/protocol/session-message-workspace.js +57 -0
- package/dist/protocol/types.d.ts +2 -0
- package/dist/session/session-message.d.ts +17 -2
- package/dist/tool-system/browser-bridge.d.ts +18 -0
- package/dist/tool-system/builtin/agent.js +23 -9
- package/dist/tool-system/builtin/browser-tools.js +18 -1
- package/dist/tool-system/builtin/index.js +3 -3
- package/dist/tool-system/builtin/send-message-to-session.js +19 -3
- package/package.json +1 -1
package/dist/protocol/server.js
CHANGED
|
@@ -17,6 +17,10 @@ import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotific
|
|
|
17
17
|
import { diskDefaultsFrom } from "../engine/engine.js";
|
|
18
18
|
import { ISOLATED_TASK_BEHAVIOR_MODE } from "../engine/run-types.js";
|
|
19
19
|
import { isProtectedSettingKey, SettingsManager } from "../settings/manager.js";
|
|
20
|
+
import { sessionMessageOutcome, sessionMessageFailure, sessionMessageResultNotification, } from "./session-message-result.js";
|
|
21
|
+
import { resolveSessionMessageWorkspace } from "./session-message-workspace.js";
|
|
22
|
+
import { validateWorkspaceContext, workspacePrimaryRoot } from "../workspace/workspace-context.js";
|
|
23
|
+
import { canonicalKey } from "../workspace/canonical-key.js";
|
|
20
24
|
import { getApprovalRouter, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
|
|
21
25
|
import { agentNotificationBus, notificationQueue, notificationEnvelopeToLegacyStreamEvent, } from "../tool-system/builtin/agent-notifications.js";
|
|
22
26
|
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
@@ -593,6 +597,13 @@ export class AgentServer {
|
|
|
593
597
|
approvalRouter: this.approvalRouter,
|
|
594
598
|
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
|
|
595
599
|
notificationMailbox: this.notificationMailbox,
|
|
600
|
+
resolveWorkspace: this.workspaceBridgeEnabled
|
|
601
|
+
? async (session) => {
|
|
602
|
+
const slice = await this.resolveHostSessionWorkspace(session, sessionId);
|
|
603
|
+
this.rememberSessionSlice(sessionId, slice);
|
|
604
|
+
return slice;
|
|
605
|
+
}
|
|
606
|
+
: undefined,
|
|
596
607
|
});
|
|
597
608
|
}
|
|
598
609
|
async rehydrateSessionForWake(sessionId) {
|
|
@@ -622,7 +633,8 @@ export class AgentServer {
|
|
|
622
633
|
});
|
|
623
634
|
return null;
|
|
624
635
|
}
|
|
625
|
-
const session = await this.chatManager.getOrCreate(sessionId, slice);
|
|
636
|
+
const session = await this.chatManager.getOrCreate(sessionId, slice, { allowReopen: false });
|
|
637
|
+
session.engine.restoreSessionModel?.(sessionId);
|
|
626
638
|
this.wireInteractiveSession(session, sessionId);
|
|
627
639
|
logger.debug("bg_wakeup.rehydrated_session", {
|
|
628
640
|
sessionId,
|
|
@@ -672,6 +684,7 @@ export class AgentServer {
|
|
|
672
684
|
}
|
|
673
685
|
/** Queue a model-sent message as an ordinary user turn in another Session. */
|
|
674
686
|
async routeSessionMessage(input) {
|
|
687
|
+
input.signal?.throwIfAborted();
|
|
675
688
|
const manager = this.chatManager;
|
|
676
689
|
if (!manager)
|
|
677
690
|
throw new Error("cross-Session messaging requires a multi-session host");
|
|
@@ -680,12 +693,49 @@ export class AgentServer {
|
|
|
680
693
|
throw new Error(`target Session is closing or closed: ${targetId}`);
|
|
681
694
|
}
|
|
682
695
|
const sourceSlice = this.lastSliceBySession.get(input.sourceSessionId);
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
696
|
+
const sourceSession = manager.get(input.sourceSessionId);
|
|
697
|
+
const assertStillAuthorized = () => {
|
|
698
|
+
input.signal?.throwIfAborted();
|
|
699
|
+
if (this.disconnected || manager.isUnavailable(targetId)) {
|
|
700
|
+
throw new Error(`target Session is closing or closed: ${targetId}`);
|
|
701
|
+
}
|
|
702
|
+
if (sourceSession &&
|
|
703
|
+
(manager.get(input.sourceSessionId) !== sourceSession ||
|
|
704
|
+
sourceSession.wasCancelledSinceLastTurn())) {
|
|
705
|
+
throw new Error("source Session was cancelled or closed before the message was queued");
|
|
706
|
+
}
|
|
686
707
|
};
|
|
708
|
+
let targetSlice;
|
|
709
|
+
if (this.workspaceBridgeEnabled) {
|
|
710
|
+
if (!sourceSession)
|
|
711
|
+
throw new Error("source Session is no longer available");
|
|
712
|
+
targetSlice = await this.resolveHostSessionWorkspace(sourceSession, targetId);
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
const sessionManager = sourceSession?.engine.getSessionManager?.() ??
|
|
716
|
+
(this.diskSessionReader ??= new SessionManager(this.sessionDiskRoot));
|
|
717
|
+
const sourceWorkspace = sourceSession?.engine.resolveSessionRunWorkspace?.(input.sourceSessionId);
|
|
718
|
+
targetSlice = resolveSessionMessageWorkspace({
|
|
719
|
+
sessionManager,
|
|
720
|
+
sourceSessionId: input.sourceSessionId,
|
|
721
|
+
targetSessionId: targetId,
|
|
722
|
+
sourceWorkspace: {
|
|
723
|
+
cwd: sourceWorkspace?.cwd ?? sourceSlice?.cwd ?? input.target.workspaceRoot,
|
|
724
|
+
workspaceContext: sourceWorkspace?.workspaceContext ?? sourceSlice?.workspaceContext,
|
|
725
|
+
projectTrusted: sourceSlice?.projectTrusted ?? false,
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
assertStillAuthorized();
|
|
687
730
|
const targetAlreadyExists = manager.sessionExistsOnDisk(targetId, targetSlice);
|
|
688
|
-
const
|
|
731
|
+
const targetWasResident = manager.get(targetId) !== undefined;
|
|
732
|
+
const targetSession = await manager.getOrCreate(targetId, targetSlice, {
|
|
733
|
+
allowReopen: false,
|
|
734
|
+
signal: input.signal,
|
|
735
|
+
});
|
|
736
|
+
assertStillAuthorized();
|
|
737
|
+
if (targetAlreadyExists && !targetWasResident)
|
|
738
|
+
targetSession.engine.restoreSessionModel?.(targetId);
|
|
689
739
|
const approvalRegistration = this.approvalRouter.register(targetId, this.connectionId);
|
|
690
740
|
if (!approvalRegistration.ok) {
|
|
691
741
|
throw new Error(`target Session ${targetId} is owned by another connection`);
|
|
@@ -693,6 +743,43 @@ export class AgentServer {
|
|
|
693
743
|
this.rememberSessionSlice(targetId, targetSlice);
|
|
694
744
|
this.observeSessionAttached(targetId, targetSession.lastActivityAt);
|
|
695
745
|
this.wireInteractiveSession(targetSession, targetId);
|
|
746
|
+
const messageId = `session-message-${nanoid(12)}`;
|
|
747
|
+
const state = { started: false, acknowledged: false, errorStreamed: false };
|
|
748
|
+
const finish = (outcome, result) => {
|
|
749
|
+
state.outcome = outcome;
|
|
750
|
+
state.result = result;
|
|
751
|
+
if (state.started)
|
|
752
|
+
this.observeRunBoundary(targetId, "end");
|
|
753
|
+
if (outcome.status !== "completed") {
|
|
754
|
+
logger.warn("session_message.turn_failed", {
|
|
755
|
+
sourceSessionId: input.sourceSessionId,
|
|
756
|
+
targetSessionId: targetId,
|
|
757
|
+
messageId,
|
|
758
|
+
error: outcome.error,
|
|
759
|
+
});
|
|
760
|
+
// Setup can return a failure without ever emitting an error event.
|
|
761
|
+
// UI observation must not prevent the durable reply from being queued.
|
|
762
|
+
try {
|
|
763
|
+
if (!state.errorStreamed) {
|
|
764
|
+
this.notify(Methods.StreamEvent, {
|
|
765
|
+
sessionId: targetId,
|
|
766
|
+
event: { type: "error", error: outcome.error ?? "cross-Session message failed" },
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
logger.warn("session_message.error_observation_failed", {
|
|
772
|
+
messageId,
|
|
773
|
+
error: String(error),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (state.acknowledged) {
|
|
778
|
+
this.notificationMailbox.enqueue(sessionMessageResultNotification(input, messageId, outcome));
|
|
779
|
+
}
|
|
780
|
+
if (result && result.turnCount > 0)
|
|
781
|
+
void this.maybeWakeIdleSession(targetId);
|
|
782
|
+
};
|
|
696
783
|
const userMessageEvent = {
|
|
697
784
|
type: "session_user_message",
|
|
698
785
|
text: input.message,
|
|
@@ -700,50 +787,43 @@ export class AgentServer {
|
|
|
700
787
|
this.observeSessionStream(targetId, userMessageEvent);
|
|
701
788
|
this.notify(Methods.StreamEvent, { sessionId: targetId, event: userMessageEvent });
|
|
702
789
|
const run = targetSession.enqueueTurn(input.message, {
|
|
703
|
-
cwd:
|
|
790
|
+
cwd: targetSlice.cwd,
|
|
791
|
+
workspaceContext: targetSlice.workspaceContext,
|
|
704
792
|
// A planned Session needs its renderer-selected initial profile. Once a
|
|
705
793
|
// Session exists, omitting this field makes Engine use the target's own
|
|
706
794
|
// persisted binding, so a stale source catalog cannot switch it back.
|
|
707
795
|
workspaceProfile: targetAlreadyExists ? undefined : input.target.workspaceProfile,
|
|
708
796
|
sessionMessageTargets: input.catalog,
|
|
709
797
|
onStream: (event) => {
|
|
798
|
+
if (event.type === "session_started" && !state.started) {
|
|
799
|
+
state.started = true;
|
|
800
|
+
this.observeRunBoundary(targetId, "start");
|
|
801
|
+
}
|
|
802
|
+
if (event.type === "error")
|
|
803
|
+
state.errorStreamed = true;
|
|
710
804
|
this.observeSessionStream(targetId, event);
|
|
711
805
|
this.notify(Methods.StreamEvent, { sessionId: targetId, event });
|
|
712
806
|
},
|
|
713
807
|
approvalRouter: this.approvalRouter,
|
|
714
808
|
});
|
|
715
|
-
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
// throws "Workspace profile ... is unavailable") used to be reported only to
|
|
719
|
-
// the log while SendMessageToSession still answered "has queued the turn".
|
|
720
|
-
// A real lead then waited two hours and re-sent, never learning the reason.
|
|
721
|
-
// So: surface an immediate failure to the caller, keep a slow turn detached.
|
|
722
|
-
let startupError;
|
|
723
|
-
const tracked = run
|
|
724
|
-
.then(() => this.maybeWakeIdleSession(targetId))
|
|
725
|
-
.catch((error) => {
|
|
726
|
-
startupError = error;
|
|
727
|
-
logger.warn("session_message.turn_failed", {
|
|
728
|
-
sourceSessionId: input.sourceSessionId,
|
|
729
|
-
targetSessionId: targetId,
|
|
730
|
-
error: error instanceof Error ? error.message : String(error),
|
|
731
|
-
});
|
|
732
|
-
this.notify(Methods.StreamEvent, {
|
|
733
|
-
sessionId: targetId,
|
|
734
|
-
event: {
|
|
735
|
-
type: "error",
|
|
736
|
-
error: error instanceof Error ? error.message : "cross-Session message failed",
|
|
737
|
-
},
|
|
738
|
-
});
|
|
739
|
-
});
|
|
740
|
-
// One macrotask is enough for a synchronous-throw path to settle; anything
|
|
741
|
-
// still running by then is genuine work and stays detached.
|
|
809
|
+
const tracked = run.then((result) => finish(sessionMessageOutcome(result), result), (error) => finish(sessionMessageFailure(error)));
|
|
810
|
+
// Acknowledge promptly, but never infer "started" from elapsed time. If
|
|
811
|
+
// setup/work is still pending, its eventual outcome takes the mailbox path.
|
|
742
812
|
await Promise.race([tracked, new Promise((resolve) => setTimeout(resolve, 0))]);
|
|
743
|
-
if (
|
|
744
|
-
|
|
813
|
+
if (state.outcome) {
|
|
814
|
+
if (state.outcome.status !== "completed")
|
|
815
|
+
throw new Error(state.outcome.error);
|
|
816
|
+
return {
|
|
817
|
+
messageId,
|
|
818
|
+
status: "completed",
|
|
819
|
+
result: { text: state.outcome.text, reason: state.result.reason },
|
|
820
|
+
};
|
|
745
821
|
}
|
|
746
|
-
|
|
822
|
+
state.acknowledged = true;
|
|
823
|
+
void tracked.catch((error) => {
|
|
824
|
+
logger.warn("session_message.result_delivery_failed", { messageId, error: String(error) });
|
|
825
|
+
});
|
|
826
|
+
return { messageId, status: state.started ? "started" : "queued" };
|
|
747
827
|
}
|
|
748
828
|
// ─── Request Dispatch ───────────────────────────────────────────
|
|
749
829
|
async handleRequest(req) {
|
|
@@ -1233,6 +1313,7 @@ export class AgentServer {
|
|
|
1233
1313
|
(params.goal != null && typeof params.goal === "object")
|
|
1234
1314
|
? params.goal
|
|
1235
1315
|
: undefined,
|
|
1316
|
+
disableGoal: params.disableGoal === true,
|
|
1236
1317
|
onStream: (event) => {
|
|
1237
1318
|
this.observeSessionStream(sid, event);
|
|
1238
1319
|
this.notify(Methods.StreamEvent, { sessionId: sid, event });
|
|
@@ -3075,13 +3156,37 @@ export class AgentServer {
|
|
|
3075
3156
|
});
|
|
3076
3157
|
});
|
|
3077
3158
|
}
|
|
3078
|
-
requestWorkspaceSwitchForSession(session, sessionId, target) {
|
|
3159
|
+
async requestWorkspaceSwitchForSession(session, sessionId, target) {
|
|
3160
|
+
return (await this.requestWorkspaceActionForSession(session, sessionId, "switch", target));
|
|
3161
|
+
}
|
|
3162
|
+
/** The same host authority resolves both dispatched turns and their reply wakeups. */
|
|
3163
|
+
async resolveHostSessionWorkspace(source, targetId) {
|
|
3164
|
+
const registration = this.approvalRouter.register(source.id, this.connectionId);
|
|
3165
|
+
if (!registration.ok)
|
|
3166
|
+
throw new Error(`source Session ${source.id} is owned by another connection`);
|
|
3167
|
+
const resolved = (await this.requestWorkspaceActionForSession(source, source.id, "resolve_session_run", targetId));
|
|
3168
|
+
if (typeof resolved.cwd !== "string" ||
|
|
3169
|
+
!resolved.cwd ||
|
|
3170
|
+
typeof resolved.projectTrusted !== "boolean") {
|
|
3171
|
+
throw new Error("host returned an invalid Session workspace");
|
|
3172
|
+
}
|
|
3173
|
+
const workspaceContext = resolved.workspaceContext === undefined
|
|
3174
|
+
? undefined
|
|
3175
|
+
: validateWorkspaceContext(resolved.workspaceContext);
|
|
3176
|
+
if (workspaceContext &&
|
|
3177
|
+
canonicalKey(workspacePrimaryRoot(workspaceContext).path) !== canonicalKey(resolved.cwd)) {
|
|
3178
|
+
throw new Error("host Session workspace does not match its working directory");
|
|
3179
|
+
}
|
|
3180
|
+
return { cwd: resolved.cwd, workspaceContext, projectTrusted: resolved.projectTrusted };
|
|
3181
|
+
}
|
|
3182
|
+
requestWorkspaceActionForSession(session, sessionId, action, target) {
|
|
3183
|
+
const label = action === "switch" ? "workspace switch" : "Session workspace resolution";
|
|
3079
3184
|
return new Promise((resolve, reject) => {
|
|
3080
3185
|
const requestId = nanoid(12);
|
|
3081
3186
|
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
3082
3187
|
this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__workspace_action__"), (decision) => {
|
|
3083
3188
|
this.clearApprovalTimer(requestId);
|
|
3084
|
-
const outcome = parseHostLoopbackDecision(decision,
|
|
3189
|
+
const outcome = parseHostLoopbackDecision(decision, label);
|
|
3085
3190
|
if (!outcome.ok) {
|
|
3086
3191
|
reject(new Error(outcome.detail));
|
|
3087
3192
|
return;
|
|
@@ -3094,11 +3199,11 @@ export class AgentServer {
|
|
|
3094
3199
|
// parsed` TypeError happened to be caught and turned into a rejection;
|
|
3095
3200
|
// this states the requirement instead of relying on that.)
|
|
3096
3201
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3097
|
-
reject(new Error(
|
|
3202
|
+
reject(new Error(`${label} ${HOST_LOOPBACK_FAILURE_DETAIL.malformed}`));
|
|
3098
3203
|
return;
|
|
3099
3204
|
}
|
|
3100
3205
|
if ("ok" in parsed && parsed.ok === false) {
|
|
3101
|
-
reject(new Error(parsed.error ??
|
|
3206
|
+
reject(new Error(parsed.error ?? `${label} failed`));
|
|
3102
3207
|
return;
|
|
3103
3208
|
}
|
|
3104
3209
|
resolve(parsed);
|
|
@@ -3108,7 +3213,7 @@ export class AgentServer {
|
|
|
3108
3213
|
this.takeSessionApproval(session, requestId, "expired");
|
|
3109
3214
|
this.pendingApprovalTargets.delete(requestId);
|
|
3110
3215
|
this.approvalTimers.delete(requestId);
|
|
3111
|
-
reject(new Error(
|
|
3216
|
+
reject(new Error(`${label} ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`));
|
|
3112
3217
|
}
|
|
3113
3218
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
3114
3219
|
this.approvalTimers.set(requestId, timer);
|
|
@@ -3116,8 +3221,8 @@ export class AgentServer {
|
|
|
3116
3221
|
...routeEnvelope,
|
|
3117
3222
|
request: {
|
|
3118
3223
|
toolName: "__workspace_action__",
|
|
3119
|
-
args: { action
|
|
3120
|
-
description: `workspace
|
|
3224
|
+
args: { action, target },
|
|
3225
|
+
description: `workspace:${action}:${target}`,
|
|
3121
3226
|
riskLevel: "low",
|
|
3122
3227
|
},
|
|
3123
3228
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EngineResult } from "../engine/types.js";
|
|
2
|
+
import type { RouteSessionMessageInput } from "../session/session-message.js";
|
|
3
|
+
import type { ResultEnvelopeDraft } from "../tool-system/builtin/agent-notifications.js";
|
|
4
|
+
export interface SessionMessageOutcome {
|
|
5
|
+
status: "completed" | "failed" | "cancelled";
|
|
6
|
+
text: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function sessionMessageFailure(error: unknown): SessionMessageOutcome;
|
|
10
|
+
/** A resolved Engine promise can still represent a refusal or failed run. */
|
|
11
|
+
export declare function sessionMessageOutcome(result: EngineResult): SessionMessageOutcome;
|
|
12
|
+
export declare function sessionMessageResultNotification(input: RouteSessionMessageInput, messageId: string, outcome: SessionMessageOutcome): ResultEnvelopeDraft;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function sessionMessageFailure(error) {
|
|
2
|
+
return {
|
|
3
|
+
status: error instanceof Error && error.name === "AbortError" ? "cancelled" : "failed",
|
|
4
|
+
text: "",
|
|
5
|
+
error: error instanceof Error ? error.message : String(error),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** A resolved Engine promise can still represent a refusal or failed run. */
|
|
9
|
+
export function sessionMessageOutcome(result) {
|
|
10
|
+
if (result.reason === "aborted_streaming" || result.reason === "aborted_tools") {
|
|
11
|
+
return {
|
|
12
|
+
status: "cancelled",
|
|
13
|
+
text: result.text,
|
|
14
|
+
error: result.text || "Target turn was cancelled.",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (result.reason === "completed" && result.turnCount > 0) {
|
|
18
|
+
return { status: "completed", text: result.text };
|
|
19
|
+
}
|
|
20
|
+
const detail = result.text ||
|
|
21
|
+
(result.turnCount === 0 ? "Target turn did not start." : "Target turn did not complete.");
|
|
22
|
+
return { status: "failed", text: result.text, error: `${detail} (reason: ${result.reason})` };
|
|
23
|
+
}
|
|
24
|
+
export function sessionMessageResultNotification(input, messageId, outcome) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "result",
|
|
27
|
+
from: { sessionId: input.target.sessionId, authority: "agent" },
|
|
28
|
+
to: { sessionId: input.sourceSessionId, authority: "system" },
|
|
29
|
+
correlationId: messageId,
|
|
30
|
+
delivery: "idle-drain",
|
|
31
|
+
payload: {
|
|
32
|
+
workId: messageId,
|
|
33
|
+
name: input.target.title,
|
|
34
|
+
description: `Reply to message ${messageId} from Session ${input.target.sessionId}: ${input.message.slice(0, 240)}`,
|
|
35
|
+
workKind: "agent",
|
|
36
|
+
status: outcome.status,
|
|
37
|
+
finalText: outcome.text,
|
|
38
|
+
...(outcome.error ? { error: outcome.error } : {}),
|
|
39
|
+
finishedAt: Date.now(),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SessionManager } from "../session/session-manager.js";
|
|
2
|
+
import { type WorkspaceContext } from "../workspace/workspace-context.js";
|
|
3
|
+
export interface SessionMessageWorkspace {
|
|
4
|
+
cwd: string;
|
|
5
|
+
workspaceContext?: WorkspaceContext;
|
|
6
|
+
projectTrusted?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Protocol-only hosts can reuse already trusted mounted roots. Desktop should
|
|
10
|
+
* ask its project registry for fresh authority instead. Persisted Session ids,
|
|
11
|
+
* bindings and runtime roots always take precedence over a caller's catalog.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveSessionMessageWorkspace(input: {
|
|
14
|
+
sessionManager: Pick<SessionManager, "readSessionMainRoot" | "readSessionProjectBinding" | "getSessionWorkspace">;
|
|
15
|
+
sourceSessionId: string;
|
|
16
|
+
targetSessionId: string;
|
|
17
|
+
sourceWorkspace: SessionMessageWorkspace;
|
|
18
|
+
targetWorkspace?: SessionMessageWorkspace;
|
|
19
|
+
}): SessionMessageWorkspace & {
|
|
20
|
+
projectTrusted: boolean;
|
|
21
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { canonicalKey } from "../workspace/canonical-key.js";
|
|
2
|
+
import { createWorkspaceContext, validateWorkspaceContext, } from "../workspace/workspace-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Protocol-only hosts can reuse already trusted mounted roots. Desktop should
|
|
5
|
+
* ask its project registry for fresh authority instead. Persisted Session ids,
|
|
6
|
+
* bindings and runtime roots always take precedence over a caller's catalog.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveSessionMessageWorkspace(input) {
|
|
9
|
+
const { sessionManager, sourceSessionId, targetSessionId, sourceWorkspace, targetWorkspace } = input;
|
|
10
|
+
const sourceMain = sessionManager.readSessionMainRoot(sourceSessionId) ?? sourceWorkspace.cwd;
|
|
11
|
+
const targetMain = sessionManager.readSessionMainRoot(targetSessionId);
|
|
12
|
+
const sourceBinding = sessionManager.readSessionProjectBinding(sourceSessionId);
|
|
13
|
+
const targetBinding = sessionManager.readSessionProjectBinding(targetSessionId);
|
|
14
|
+
const cwd = sessionManager.getSessionWorkspace(targetSessionId)?.root ?? targetMain ?? sourceMain;
|
|
15
|
+
const sameMainRoot = !targetMain || canonicalKey(sourceMain) === canonicalKey(targetMain);
|
|
16
|
+
const projectTrusted = targetWorkspace?.projectTrusted ?? (sameMainRoot && sourceWorkspace.projectTrusted === true);
|
|
17
|
+
if (targetMain && !targetBinding) {
|
|
18
|
+
if (!sameMainRoot)
|
|
19
|
+
throw new Error("target Session does not share the source's authorized main root");
|
|
20
|
+
return { cwd, projectTrusted };
|
|
21
|
+
}
|
|
22
|
+
if (!targetBinding && !sourceBinding)
|
|
23
|
+
return { cwd, projectTrusted };
|
|
24
|
+
if (targetBinding && (!sourceBinding || targetBinding.projectId !== sourceBinding.projectId)) {
|
|
25
|
+
throw new Error("target Session project binding is not authorized by the source Session");
|
|
26
|
+
}
|
|
27
|
+
const binding = targetBinding ?? sourceBinding;
|
|
28
|
+
if (!sourceWorkspace.workspaceContext) {
|
|
29
|
+
throw new Error("cross-Session run requires an authoritative WorkspaceContext from the host");
|
|
30
|
+
}
|
|
31
|
+
const context = validateWorkspaceContext(sourceWorkspace.workspaceContext);
|
|
32
|
+
if (context.projectId !== sourceBinding?.projectId ||
|
|
33
|
+
context.sessionMainRootId !== sourceBinding.mainRootId) {
|
|
34
|
+
throw new Error("source WorkspaceContext does not match its persisted project binding");
|
|
35
|
+
}
|
|
36
|
+
// A source worktree is a runtime substitution, not a project mount that may
|
|
37
|
+
// be propagated into another Session's secondary roots.
|
|
38
|
+
const mountedRoots = context.roots.map((root) => root.id === sourceBinding.mainRootId ? { ...root, path: sourceMain } : { ...root });
|
|
39
|
+
const targetRoot = mountedRoots.find((root) => root.id === binding.mainRootId);
|
|
40
|
+
if (!targetRoot || (targetMain && canonicalKey(targetRoot.path) !== canonicalKey(targetMain))) {
|
|
41
|
+
throw new Error("target Session main root is not present in the host-authorized project roots");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
cwd,
|
|
45
|
+
projectTrusted,
|
|
46
|
+
workspaceContext: createWorkspaceContext({
|
|
47
|
+
projectId: context.projectId,
|
|
48
|
+
projectRevision: context.projectRevision,
|
|
49
|
+
sessionMainRootId: binding.mainRootId,
|
|
50
|
+
roots: mountedRoots.map((root) => ({
|
|
51
|
+
...root,
|
|
52
|
+
path: root.id === binding.mainRootId ? cwd : root.path,
|
|
53
|
+
role: root.id === binding.mainRootId ? "primary" : "secondary",
|
|
54
|
+
})),
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -198,6 +198,8 @@ export interface RunParams {
|
|
|
198
198
|
* optional token/time budgets). Normalized at the engine run boundary.
|
|
199
199
|
*/
|
|
200
200
|
goal?: string | import("../goal/lifecycle.js").GoalConfig;
|
|
201
|
+
/** Disable explicit, persisted, and configured Goal mode for this standalone turn. */
|
|
202
|
+
disableGoal?: boolean;
|
|
201
203
|
}
|
|
202
204
|
export interface RunResult {
|
|
203
205
|
text: string;
|
|
@@ -9,14 +9,29 @@ export interface RouteSessionMessageInput {
|
|
|
9
9
|
sourceSessionId: string;
|
|
10
10
|
target: SessionMessageTarget;
|
|
11
11
|
message: string;
|
|
12
|
+
/** Sender tool cancellation; never serialized or supplied by the model. */
|
|
13
|
+
signal?: AbortSignal;
|
|
12
14
|
/** Full same-project catalog so the target can send a later message onward. */
|
|
13
15
|
catalog: readonly SessionMessageTarget[];
|
|
14
16
|
}
|
|
15
|
-
|
|
17
|
+
/** One dispatch acknowledgement, not a subscription to future target turns. */
|
|
18
|
+
export interface SessionMessageReceipt {
|
|
19
|
+
messageId: string;
|
|
20
|
+
status: "queued" | "started" | "completed";
|
|
21
|
+
/** Present when the target finished before the dispatch returned. */
|
|
22
|
+
result?: {
|
|
23
|
+
text: string;
|
|
24
|
+
reason: import("../types.js").TerminalReason;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export type SessionMessageRouter = (input: RouteSessionMessageInput) => Promise<SessionMessageReceipt | void>;
|
|
16
28
|
export interface SessionMessageToolService {
|
|
17
29
|
targets: readonly SessionMessageTarget[];
|
|
18
30
|
send(input: {
|
|
19
31
|
targetSessionId: string;
|
|
20
32
|
message: string;
|
|
21
|
-
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}): Promise<SessionMessageTarget & {
|
|
35
|
+
receipt?: SessionMessageReceipt;
|
|
36
|
+
}>;
|
|
22
37
|
}
|
|
@@ -25,6 +25,22 @@ export interface BrowserElement {
|
|
|
25
25
|
/** Optional value for non-sensitive inputs (e.g. current textbox text). */
|
|
26
26
|
value?: string;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Which browser identity a page is being viewed as.
|
|
30
|
+
*
|
|
31
|
+
* The model cannot otherwise tell a throwaway sandbox partition from the user's
|
|
32
|
+
* real logged-in browser, and the consequences of a click differ sharply
|
|
33
|
+
* between them. Optional so a host that only ever drives its own sandbox need
|
|
34
|
+
* not supply it. See docs/todo/browser-profile-workspace-lease-design.md §8.3.
|
|
35
|
+
*/
|
|
36
|
+
export interface BrowserIdentity {
|
|
37
|
+
/** Profile the partition is derived from (e.g. "p:<project>" / "u:<name>"). */
|
|
38
|
+
profileId: string;
|
|
39
|
+
/** Where the browser itself comes from. */
|
|
40
|
+
sourceKind?: "builtin-panel" | "builtin-headless" | "attached-chrome" | "remote";
|
|
41
|
+
/** True when this is the user's own browser, carrying their real logins. */
|
|
42
|
+
isUserBrowser?: boolean;
|
|
43
|
+
}
|
|
28
44
|
export interface BrowserSnapshot {
|
|
29
45
|
url: string;
|
|
30
46
|
title?: string;
|
|
@@ -38,6 +54,8 @@ export interface BrowserSnapshot {
|
|
|
38
54
|
/** Set when the page needs the user to act (login wall / 2FA) — agent should
|
|
39
55
|
* hand control back to the user rather than retry. */
|
|
40
56
|
needsHuman?: string;
|
|
57
|
+
/** Which login identity this page is being viewed as (§8.3). */
|
|
58
|
+
identity?: BrowserIdentity;
|
|
41
59
|
}
|
|
42
60
|
export type BrowserResultCode = "OK" | "STALE_SNAPSHOT" | "STALE_CURSOR" | "NO_PROGRESS" | "NAVIGATION" | "BLOCKED" | "NEEDS_HUMAN" | "FAILED";
|
|
43
61
|
export interface BrowserScrollState {
|
|
@@ -257,8 +257,8 @@ async function runSubAgent(spawner, opts,
|
|
|
257
257
|
uiStream,
|
|
258
258
|
/**
|
|
259
259
|
* Optional override for the spawned child Engine's per-event stream.
|
|
260
|
-
* Background path
|
|
261
|
-
*
|
|
260
|
+
* Background path mirrors per-event detail into its transcript and the
|
|
261
|
+
* parent stream, where agentId keeps it scoped to the child card. Sync
|
|
262
262
|
* calls leave undefined; engine.ts falls back to `spawner.parentStream`
|
|
263
263
|
* (the parent UI), preserving the inline rendering of synchronous
|
|
264
264
|
* sub-agents.
|
|
@@ -388,12 +388,27 @@ export async function agentTool(args, ctx) {
|
|
|
388
388
|
// (per-instance closure state), so it doesn't interleave with other
|
|
389
389
|
// agents.
|
|
390
390
|
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
// 4th arg to runSubAgent); per-event detail goes through the
|
|
394
|
-
// `streamOverride` (5th arg → SubAgentSpawnRequest.streamOverride →
|
|
395
|
-
// engine.ts spawn closure routes to it instead of the main UI).
|
|
391
|
+
// Also forward the agent-scoped events so a desktop child card can show
|
|
392
|
+
// live operations even after the spawning parent turn has finished.
|
|
396
393
|
const transcriptSink = createTranscriptTranslator(agentId);
|
|
394
|
+
const detailSink = (event) => {
|
|
395
|
+
transcriptSink(event);
|
|
396
|
+
safeEmit(parentStream, event);
|
|
397
|
+
};
|
|
398
|
+
const lifecycleSink = (event) => {
|
|
399
|
+
safeEmit(parentStream, event);
|
|
400
|
+
if (event.type === "agent_start") {
|
|
401
|
+
// Mark detached before spawn can emit any detail. Otherwise the parent
|
|
402
|
+
// turn's completion sweep prematurely seals this still-running card.
|
|
403
|
+
safeEmit(parentStream, {
|
|
404
|
+
type: "agent_backgrounded",
|
|
405
|
+
agentId,
|
|
406
|
+
name,
|
|
407
|
+
description,
|
|
408
|
+
agentType: overrides.resolvedType,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
};
|
|
397
412
|
void runSubAgent(spawner, {
|
|
398
413
|
agentId,
|
|
399
414
|
name,
|
|
@@ -429,8 +444,7 @@ export async function agentTool(args, ctx) {
|
|
|
429
444
|
onProgressEvent: progressTracker.onEvent,
|
|
430
445
|
onAgentProgress: progressTracker.onRuntime,
|
|
431
446
|
signal: controller.signal,
|
|
432
|
-
},
|
|
433
|
-
transcriptSink)
|
|
447
|
+
}, lifecycleSink, detailSink)
|
|
434
448
|
.then((text) => {
|
|
435
449
|
if (controller.signal.aborted) {
|
|
436
450
|
if (liveLease)
|
|
@@ -30,6 +30,23 @@ function bridge(ctx) {
|
|
|
30
30
|
return ctx?.browser;
|
|
31
31
|
}
|
|
32
32
|
const STALE = (ref) => `Error: ref ${ref} is no longer valid (page changed). Re-run browser_observe.`;
|
|
33
|
+
/**
|
|
34
|
+
* One line naming the login identity this page is being viewed as.
|
|
35
|
+
*
|
|
36
|
+
* A profile id alone does not convey risk, so the user's own browser gets an
|
|
37
|
+
* explicit warning: actions there hit their real accounts, not a sandbox.
|
|
38
|
+
* Renders nothing when the host reports no identity, so hosts that only drive
|
|
39
|
+
* their own sandbox are unaffected.
|
|
40
|
+
*/
|
|
41
|
+
function renderIdentity(identity) {
|
|
42
|
+
if (!identity)
|
|
43
|
+
return "";
|
|
44
|
+
const source = identity.sourceKind ? ` (${identity.sourceKind})` : "";
|
|
45
|
+
const warning = identity.isUserBrowser
|
|
46
|
+
? " — this is the user's own browser: actions here affect their real accounts"
|
|
47
|
+
: "";
|
|
48
|
+
return `\nIdentity: ${identity.profileId}${source}${warning}`;
|
|
49
|
+
}
|
|
33
50
|
// ════════════════════════════════════════════════════════════════════════════
|
|
34
51
|
// browser_observe — observe the page (snapshot / read / extract)
|
|
35
52
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -103,7 +120,7 @@ export async function browserObserveTool(args, ctx) {
|
|
|
103
120
|
const snap = await b.snapshot();
|
|
104
121
|
if (snap.detail)
|
|
105
122
|
return `Error: ${snap.detail}`;
|
|
106
|
-
const header = `URL: ${snap.url}${snap.title ? `\nTitle: ${snap.title}` : ""}`;
|
|
123
|
+
const header = `URL: ${snap.url}${snap.title ? `\nTitle: ${snap.title}` : ""}${renderIdentity(snap.identity)}`;
|
|
107
124
|
const human = snap.needsHuman
|
|
108
125
|
? `\n\n⚠ ${snap.needsHuman} — please complete it in the browser window, then continue.`
|
|
109
126
|
: "";
|
|
@@ -41,7 +41,7 @@ import { sendMessageToSessionToolDef, sendMessageToSessionTool, rewriteSendMessa
|
|
|
41
41
|
import { useCredentialToolDef, useCredentialBuiltinTool, } from "../../credentials/use-credential-tool.js";
|
|
42
42
|
import { injectCredentialToolDef, injectCredentialTool, isInjectCredentialAvailable, } from "../../credentials/inject-credential-tool.js";
|
|
43
43
|
import { credentialAccessScope, getCredentialAccess } from "../../credentials/access.js";
|
|
44
|
-
import { linkActionToolDef, linkActionTool
|
|
44
|
+
import { linkActionToolDef, linkActionTool } from "../../links/link-action-tool.js";
|
|
45
45
|
const FAILURE_TEXT = /^(?:error\b|failed\b|failure\b|skill\b.*(?:not found\b|disabled\b|not available\b|allowlist\b|denied\b)|(?:[a-z][\w-]*\s+){1,3}(?:error\b|failed\b|failure\b|aborted\b|timed out\b))/i;
|
|
46
46
|
function failureMessage(result) {
|
|
47
47
|
if (typeof result.error === "string" && result.error)
|
|
@@ -831,7 +831,8 @@ const BUILTIN_CONTRIBUTIONS = [
|
|
|
831
831
|
availability: (ctx) => isUseCredentialAvailable(ctx.cwd, ctx.settingsScope),
|
|
832
832
|
}),
|
|
833
833
|
},
|
|
834
|
-
//
|
|
834
|
+
// Link discovery remains available even when no saved connection is usable.
|
|
835
|
+
// Actions resolve their provider-owned credential on every call.
|
|
835
836
|
// Read actions are allowed after the user connects a provider; write actions
|
|
836
837
|
// use a closed, in-tool approval prompt so read and write share one catalog.
|
|
837
838
|
{
|
|
@@ -845,7 +846,6 @@ const BUILTIN_CONTRIBUTIONS = [
|
|
|
845
846
|
execute: linkActionTool,
|
|
846
847
|
exposure: expose(GENERAL_TAGS, {
|
|
847
848
|
defaultPermissionRules: allow(linkActionToolDef.name),
|
|
848
|
-
availability: (ctx) => isLinkActionAvailable(ctx.cwd, ctx.settingsScope),
|
|
849
849
|
}),
|
|
850
850
|
},
|
|
851
851
|
// InjectCredential:把 cookie 凭证注入内置浏览器(恢复登录态)。审批由工具内部
|