@agent-link/agent 0.1.343 → 0.1.345

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/copilot.js CHANGED
@@ -360,6 +360,7 @@ function sessionRef(sessionId) {
360
360
  };
361
361
  }
362
362
  function startCopilotTurn(state) {
363
+ state.steeringRequests = undefined;
363
364
  state.toolStartKeys = new Set();
364
365
  state.compactionRecord = undefined;
365
366
  state.reviewDocuments = new CopilotReviewDocuments();
@@ -418,6 +419,7 @@ function startCopilotTurn(state) {
418
419
  }
419
420
  }
420
421
  function finishCopilotTurn(state) {
422
+ settleCopilotSteering(state, 'failed');
421
423
  settleCopilotUserRequest(state, 'failed');
422
424
  state.turnActive = false;
423
425
  state.lastActivityAt = Date.now();
@@ -882,6 +884,171 @@ export function getCopilotConversation(conversationId) {
882
884
  conversationAliases.delete(conversationId);
883
885
  return undefined;
884
886
  }
887
+ function copilotSteeringReceipt(state, request) {
888
+ const sessionId = state.commandSessionId || state.copilotSessionId || state.lastCopilotSessionId;
889
+ return { type: 'backend_event', providerId: request.providerId, conversationId: state.conversationId,
890
+ workDir: state.workDir, claudeSessionId: sessionId, turnId: request.backendTurnId, stateVersion: state.stateVersion,
891
+ event: { type: 'user_message', session: sessionRef(sessionId || 'pending'), message: { ...request } } };
892
+ }
893
+ export function getCopilotSteeringReceipts() {
894
+ return [...conversations.values()].flatMap(state => [...(state.steeringRequests?.values() || [])]
895
+ .filter(request => request.providerId === state.providerId && request.backendTurnId === state.turnId)
896
+ .map(request => copilotSteeringReceipt(state, request)));
897
+ }
898
+ function publishCopilotSteering(state, request) {
899
+ const sessionId = state.commandSessionId || state.copilotSessionId || state.lastCopilotSessionId;
900
+ try {
901
+ if (sessionId)
902
+ saveCopilotUserRequest(COPILOT_SESSION_DIR, sessionId, request);
903
+ }
904
+ finally {
905
+ sendFn(copilotSteeringReceipt(state, request));
906
+ }
907
+ }
908
+ function settleCopilotSteering(state, status) {
909
+ for (const request of state.steeringRequests?.values() || []) {
910
+ if (request.copilotRequestState !== 'pending')
911
+ continue;
912
+ request.copilotRequestState = status;
913
+ try {
914
+ publishCopilotSteering(state, request);
915
+ }
916
+ catch (error) {
917
+ console.warn('[Copilot:steering] Receipt persistence failed:', error);
918
+ }
919
+ }
920
+ }
921
+ export async function handleCopilotSteering(prompt, scope, send) {
922
+ let state;
923
+ let request;
924
+ try {
925
+ if (typeof prompt !== 'string' || !prompt.trim() || prompt.trim().startsWith('/')
926
+ || typeof scope.requestId !== 'string' || !/^[\w-]{1,100}$/.test(scope.requestId)) {
927
+ throw new Error('Steering requires text and a valid request ID');
928
+ }
929
+ state = getCopilotConversation(scope.conversationId);
930
+ const sessionId = state?.commandSessionId;
931
+ if (!state || scope.providerId !== 'copilot' || state.providerId !== scope.providerId
932
+ || !sessionId || scope.claudeSessionId !== sessionId || typeof scope.workDir !== 'string'
933
+ || resolve(scope.workDir) !== resolve(state.workDir) || !scope.turnId || scope.turnId !== state.turnId) {
934
+ throw new Error('Steering conversation, provider, directory, session or turn no longer matches');
935
+ }
936
+ state.steeringRequests ??= new Map();
937
+ const existing = state.steeringRequests.get(scope.requestId)
938
+ || readCopilotUserRequests(COPILOT_SESSION_DIR, sessionId).find(row => row.copilotRequestId === scope.requestId);
939
+ if (existing) {
940
+ if (!existing.copilotSteering || existing.content !== prompt.trim() || existing.backendTurnId !== scope.turnId) {
941
+ publishCopilotSteering(state, existing);
942
+ send({ type: 'error', operation: 'copilot_request', providerId: scope.providerId,
943
+ conversationId: scope.conversationId, preserveActiveTurn: true,
944
+ message: 'Steering request ID was already used for a different request' });
945
+ return;
946
+ }
947
+ publishCopilotSteering(state, existing);
948
+ return;
949
+ }
950
+ if (!state.turnActive || state.isCompacting || state.copilotMode !== 'interactive'
951
+ || state.planMode || state.planModePending || state.activeOperation !== 'chat'
952
+ || getPendingCopilotDecisions(state.conversationId).length || !state.commandRuntime?.isRunning()) {
953
+ throw new Error('Steering is unavailable: the interactive task is no longer accepting requirements');
954
+ }
955
+ request = { role: 'user', content: prompt.trim(), copilotRequestId: scope.requestId,
956
+ backendTurnId: scope.turnId, providerId: scope.providerId, copilotSteering: true,
957
+ copilotRequestState: 'pending', timestamp: new Date().toISOString() };
958
+ state.steeringRequests.set(scope.requestId, request);
959
+ publishCopilotSteering(state, request);
960
+ const runtime = state.commandRuntime;
961
+ const nativeId = await runtime.steer(sessionId, request.content);
962
+ if (state.commandRuntime !== runtime || state.commandSessionId !== sessionId || state.turnId !== scope.turnId
963
+ || state.providerId !== scope.providerId || resolve(state.workDir) !== resolve(scope.workDir)) {
964
+ console.warn('[Copilot:steering] Ignoring acknowledgment after ownership changed');
965
+ return;
966
+ }
967
+ request.copilotUserMessageId = nativeId;
968
+ request.copilotRequestDispatched = true;
969
+ publishCopilotSteering(state, request);
970
+ }
971
+ catch (error) {
972
+ const message = error instanceof Error ? error.message : String(error);
973
+ console.warn('[Copilot:steering]', message);
974
+ if (request && state && state.commandSessionId === scope.claudeSessionId && state.turnId === scope.turnId
975
+ && state.providerId === scope.providerId && resolve(state.workDir) === resolve(scope.workDir)) {
976
+ if (request.copilotRequestState === 'pending')
977
+ request.copilotRequestState = 'failed';
978
+ request.copilotSteeringError = message;
979
+ try {
980
+ publishCopilotSteering(state, request);
981
+ }
982
+ catch (persistError) {
983
+ console.warn('[Copilot:steering] Failed receipt persistence:', persistError);
984
+ }
985
+ }
986
+ else {
987
+ send({ type: 'backend_event', providerId: scope.providerId, conversationId: scope.conversationId,
988
+ workDir: scope.workDir, claudeSessionId: scope.claudeSessionId, turnId: scope.turnId,
989
+ event: { type: 'user_message', session: sessionRef(scope.claudeSessionId),
990
+ message: { role: 'user', content: typeof prompt === 'string' ? prompt : '', copilotSteering: true,
991
+ copilotRequestId: scope.requestId, copilotRequestState: 'failed', copilotSteeringError: message,
992
+ timestamp: new Date().toISOString() } } });
993
+ }
994
+ }
995
+ }
996
+ export async function handleCopilotBtwQuestion(question, scope, send) {
997
+ const reply = (message) => send({ type: 'btw_answer', ...scope, ...message });
998
+ try {
999
+ if (scope.providerId !== 'copilot') {
1000
+ reply({ delta: '', done: true, errorCode: 'btw_unsupported' });
1001
+ return;
1002
+ }
1003
+ if (!scope.requestId || typeof question !== 'string' || !question.trim()) {
1004
+ reply({ delta: '', done: true, errorCode: 'btw_invalid' });
1005
+ return;
1006
+ }
1007
+ const conversationId = scope.conversationId;
1008
+ const state = conversationId ? getCopilotConversation(conversationId) : undefined;
1009
+ const sessionId = state?.commandSessionId || state?.copilotSessionId || state?.lastCopilotSessionId;
1010
+ if (!conversationId || !state || !sessionId || !scope.claudeSessionId || scope.claudeSessionId !== sessionId
1011
+ || state.providerId !== scope.providerId || resolve(state.workDir) !== resolve(scope.workDir)) {
1012
+ reply({ delta: '', done: true, errorCode: 'btw_no_session' });
1013
+ return;
1014
+ }
1015
+ markCopilotRuntimeActivity(state);
1016
+ if (state.runtimeInitPromise)
1017
+ await state.runtimeInitPromise;
1018
+ if (!state.commandRuntime?.isRunning()) {
1019
+ if (state.turnActive || state.isCompacting) {
1020
+ reply({ delta: '', done: true, errorCode: 'btw_unavailable' });
1021
+ return;
1022
+ }
1023
+ await ensureCopilotRuntime(state, sessionId);
1024
+ }
1025
+ const runtime = state.commandRuntime;
1026
+ const stillCurrent = () => getCopilotConversation(conversationId) === state
1027
+ && state.providerId === scope.providerId
1028
+ && resolve(state.workDir) === resolve(scope.workDir)
1029
+ && state.commandRuntime === runtime && state.commandSessionId === sessionId;
1030
+ if (!runtime || !stillCurrent()) {
1031
+ reply({ delta: '', done: true, errorCode: 'btw_unavailable' });
1032
+ return;
1033
+ }
1034
+ const answer = await runtime.ephemeralQuery(sessionId, question.trim(), delta => {
1035
+ if (stillCurrent())
1036
+ reply({ delta, done: false });
1037
+ });
1038
+ if (stillCurrent()) {
1039
+ markCopilotRuntimeActivity(state);
1040
+ reply({ answer, delta: '', done: true });
1041
+ }
1042
+ else
1043
+ reply({ delta: '', done: true, errorCode: 'btw_unavailable' });
1044
+ }
1045
+ catch (error) {
1046
+ console.warn(`[Copilot:btw] ${error instanceof Error ? error.message : String(error)}`);
1047
+ const errorCode = error instanceof Error && 'code' in error && error.code === 'btw_busy'
1048
+ ? 'btw_busy' : 'btw_failed';
1049
+ reply({ delta: '', done: true, errorCode, error: error instanceof Error ? error.message : String(error) });
1050
+ }
1051
+ }
885
1052
  function rejectCopilotBusyRequest(conversationId, state) {
886
1053
  // Reject this extra request, not the still-running turn shared by other pages.
887
1054
  // Existing terminal-turn guards must not interpret it as the owner's failure.
@@ -3351,7 +3518,20 @@ function handleCopilotEvent(state, event) {
3351
3518
  const id = data?.messageId || event.id;
3352
3519
  if (typeof id !== 'string' || (!message && !attachments.length))
3353
3520
  break;
3354
- if (state.userRequest?.copilotRequestState === 'pending') {
3521
+ const steering = [...(state.steeringRequests?.values() || [])].find(row => row.copilotUserMessageId === id)
3522
+ || [...(state.steeringRequests?.values() || [])].find(row => !row.copilotUserMessageId
3523
+ && row.content === content && ['steering', 'queued'].includes(data?.delivery));
3524
+ if (steering) {
3525
+ steering.copilotUserMessageId = id;
3526
+ steering.copilotRequestState = 'delivered';
3527
+ try {
3528
+ publishCopilotSteering(state, steering);
3529
+ }
3530
+ catch (error) {
3531
+ console.warn('[Copilot:steering] Could not save delivery:', error);
3532
+ }
3533
+ }
3534
+ if (state.userRequest?.copilotRequestState === 'pending' && data?.delivery !== 'steering' && !steering) {
3355
3535
  state.userRequest.copilotUserMessageId = id;
3356
3536
  settleCopilotUserRequest(state, 'delivered');
3357
3537
  }
@@ -3360,6 +3540,7 @@ function handleCopilotEvent(state, event) {
3360
3540
  ...(state.userRequest?.copilotUserMessageId === id ? {
3361
3541
  copilotRequestId: state.userRequest.copilotRequestId, copilotRequestState: 'delivered',
3362
3542
  } : {}),
3543
+ ...(steering ? { ...steering } : data?.delivery === 'steering' ? { copilotSteering: true } : {}),
3363
3544
  copilotUserMessageId: id, ...(attachments.length ? { attachments } : {}) } });
3364
3545
  break;
3365
3546
  }
@@ -3817,6 +3998,8 @@ function handleCopilotEvent(state, event) {
3817
3998
  // ── Cancel ──────────────────────────────────────────────────────────────────
3818
3999
  export function cancelCopilotExecution(conversationId) {
3819
4000
  const state = getCopilotConversation(conversationId);
4001
+ if (state)
4002
+ settleCopilotSteering(state, 'cancelled');
3820
4003
  if (state)
3821
4004
  settleCopilotUserRequest(state, 'cancelled');
3822
4005
  cancelPendingCopilotDecisions(state?.conversationId || conversationId);
@@ -5528,7 +5711,10 @@ function mergeCopilotRequestHistory(messages, sessionId) {
5528
5711
  else
5529
5712
  requests[index] = owner.userRequest;
5530
5713
  }
5531
- return mergeCopilotUserRequests(messages, requests, owner?.userRequest?.copilotRequestId);
5714
+ const liveIds = [owner?.userRequest?.copilotRequestId,
5715
+ ...[...(owner?.steeringRequests?.values() || [])].map(row => row.copilotRequestId)]
5716
+ .filter((id) => !!id);
5717
+ return mergeCopilotUserRequests(messages, requests, liveIds);
5532
5718
  }
5533
5719
  export function readCopilotSessionMode(sessionId) {
5534
5720
  const sessionRoot = resolve(COPILOT_SESSION_DIR);