@ai-sdk/harness-opencode 1.0.41 → 1.0.43
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/CHANGELOG.md +16 -0
- package/dist/bridge/index.mjs +447 -404
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +512 -0
- package/src/bridge/index.ts +54 -495
- package/src/bridge/opencode-events.ts +22 -0
package/src/bridge/index.ts
CHANGED
|
@@ -15,9 +15,7 @@ import {
|
|
|
15
15
|
} from '@opencode-ai/sdk/v2';
|
|
16
16
|
import {
|
|
17
17
|
createTranslationState,
|
|
18
|
-
|
|
19
|
-
emitLegacyTextPartUpdate,
|
|
20
|
-
emitMissingFinalDelta,
|
|
18
|
+
emitOpenCodeStreamStart,
|
|
21
19
|
getOpenCodeEventSessionId,
|
|
22
20
|
isStepSettlementEvent,
|
|
23
21
|
type OpenCodeEvent,
|
|
@@ -25,9 +23,11 @@ import {
|
|
|
25
23
|
unwrapOpenCodeEvent,
|
|
26
24
|
} from './opencode-events';
|
|
27
25
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
createEmitStreamEvent,
|
|
27
|
+
isRecord,
|
|
28
|
+
stringValue,
|
|
29
|
+
} from './create-emit-stream-event';
|
|
30
|
+
import { mapOpenCodeFinishReason } from './opencode-finish-step';
|
|
31
31
|
import { prependOpenCodeBinToPath } from './opencode-path';
|
|
32
32
|
import {
|
|
33
33
|
addUsage,
|
|
@@ -485,30 +485,6 @@ function legacyRetryStatusMessage(event: OpenCodeEvent): string {
|
|
|
485
485
|
: 'OpenCode session retry';
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
function nextRetryEventMessage(event: OpenCodeEvent): string {
|
|
489
|
-
const props = event.properties ?? {};
|
|
490
|
-
const details: string[] = [];
|
|
491
|
-
if (typeof props.attempt === 'number') {
|
|
492
|
-
details.push(`attempt ${props.attempt}`);
|
|
493
|
-
}
|
|
494
|
-
const error = props.error;
|
|
495
|
-
if (isRecord(error)) {
|
|
496
|
-
const message =
|
|
497
|
-
stringValue(error.message) ??
|
|
498
|
-
(isRecord(error.data) ? stringValue(error.data.message) : undefined);
|
|
499
|
-
const statusCode = error.statusCode;
|
|
500
|
-
if (typeof statusCode === 'number') {
|
|
501
|
-
details.push(`HTTP ${statusCode}`);
|
|
502
|
-
}
|
|
503
|
-
if (message) details.push(message);
|
|
504
|
-
} else if (error != null) {
|
|
505
|
-
details.push(formatError(error));
|
|
506
|
-
}
|
|
507
|
-
return details.length > 0
|
|
508
|
-
? `OpenCode session retry: ${details.join('; ')}`
|
|
509
|
-
: 'OpenCode session retry';
|
|
510
|
-
}
|
|
511
|
-
|
|
512
488
|
async function ensureSession({
|
|
513
489
|
client,
|
|
514
490
|
start,
|
|
@@ -562,6 +538,7 @@ async function runPrompt({
|
|
|
562
538
|
let sawFinishStep = false;
|
|
563
539
|
let sawBusy = false;
|
|
564
540
|
let terminalError: string | undefined;
|
|
541
|
+
const state = createTranslationState();
|
|
565
542
|
const initialSessionTokens = await readSessionTokens({
|
|
566
543
|
client,
|
|
567
544
|
sessionId,
|
|
@@ -575,6 +552,7 @@ async function runPrompt({
|
|
|
575
552
|
permissionMode: start.permissionMode,
|
|
576
553
|
builtinToolFiltering: start.builtinToolFiltering,
|
|
577
554
|
turn,
|
|
555
|
+
state,
|
|
578
556
|
emit: msg => {
|
|
579
557
|
if (msg.type === 'text-delta' || msg.type === 'reasoning-delta') {
|
|
580
558
|
sawContent = true;
|
|
@@ -591,6 +569,13 @@ async function runPrompt({
|
|
|
591
569
|
signal: eventsAbort.signal,
|
|
592
570
|
onSubscribed: () => eventsReady.resolve(undefined),
|
|
593
571
|
onEvent: event => {
|
|
572
|
+
if (event.type === 'message.updated') {
|
|
573
|
+
emitOpenCodeStreamStart({
|
|
574
|
+
info: event.properties?.info,
|
|
575
|
+
state,
|
|
576
|
+
emit,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
594
579
|
if (event.type === 'session.updated') {
|
|
595
580
|
latestSessionTokens =
|
|
596
581
|
extractSessionTokens(event.properties) ?? latestSessionTokens;
|
|
@@ -619,10 +604,6 @@ async function runPrompt({
|
|
|
619
604
|
eventsReady.resolve(undefined);
|
|
620
605
|
turnSettled.resolve();
|
|
621
606
|
});
|
|
622
|
-
emit({
|
|
623
|
-
type: 'stream-start',
|
|
624
|
-
...(start.model ? { modelId: start.model } : {}),
|
|
625
|
-
});
|
|
626
607
|
await eventsReady.promise;
|
|
627
608
|
const prompted = await legacySessionPrompt({
|
|
628
609
|
client,
|
|
@@ -641,6 +622,7 @@ async function runPrompt({
|
|
|
641
622
|
const emittedFallback = await emitContextFallback({
|
|
642
623
|
client,
|
|
643
624
|
sessionId,
|
|
625
|
+
state,
|
|
644
626
|
emit,
|
|
645
627
|
emitContent: !sawContent,
|
|
646
628
|
}).catch(() => false);
|
|
@@ -701,6 +683,7 @@ async function runCompaction({
|
|
|
701
683
|
permissionMode: start.permissionMode,
|
|
702
684
|
builtinToolFiltering: start.builtinToolFiltering,
|
|
703
685
|
turn,
|
|
686
|
+
state: createTranslationState(),
|
|
704
687
|
emit: msg => {
|
|
705
688
|
if (msg.type === 'compaction') sawCompaction = true;
|
|
706
689
|
emit(msg);
|
|
@@ -764,6 +747,7 @@ async function consumeEvents({
|
|
|
764
747
|
permissionMode,
|
|
765
748
|
builtinToolFiltering,
|
|
766
749
|
turn,
|
|
750
|
+
state,
|
|
767
751
|
emit,
|
|
768
752
|
signal,
|
|
769
753
|
onSubscribed,
|
|
@@ -774,6 +758,7 @@ async function consumeEvents({
|
|
|
774
758
|
permissionMode: StartMessage['permissionMode'];
|
|
775
759
|
builtinToolFiltering: StartMessage['builtinToolFiltering'];
|
|
776
760
|
turn: BridgeTurn;
|
|
761
|
+
state: TranslationState;
|
|
777
762
|
emit: Emit;
|
|
778
763
|
signal: AbortSignal;
|
|
779
764
|
onSubscribed?: () => void;
|
|
@@ -782,472 +767,47 @@ async function consumeEvents({
|
|
|
782
767
|
const stream = await subscribeLegacyEvents({ client, signal });
|
|
783
768
|
onSubscribed?.();
|
|
784
769
|
if (!stream) return;
|
|
785
|
-
const
|
|
770
|
+
const emitStreamEvent = createEmitStreamEvent({
|
|
771
|
+
state,
|
|
772
|
+
emit,
|
|
773
|
+
emitWarning: turn.emitWarning,
|
|
774
|
+
emitError: turn.emitError,
|
|
775
|
+
toWireToolName,
|
|
776
|
+
nativeNameField,
|
|
777
|
+
getHostToolName,
|
|
778
|
+
authorizeHostToolCall: input => authorizeHostToolCall({ ...input, state }),
|
|
779
|
+
stripWorkDir,
|
|
780
|
+
formatError,
|
|
781
|
+
});
|
|
786
782
|
for await (const rawEvent of stream) {
|
|
787
783
|
if (signal.aborted || turn.abortSignal.aborted) break;
|
|
788
784
|
const event = unwrapOpenCodeEvent(rawEvent);
|
|
789
785
|
const eventSessionId = event ? getOpenCodeEventSessionId(event) : undefined;
|
|
790
786
|
if (!event || (eventSessionId && eventSessionId !== sessionId)) continue;
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
});
|
|
801
|
-
if (onEvent?.(event)) break;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
async function translateAndEmit({
|
|
806
|
-
event,
|
|
807
|
-
state,
|
|
808
|
-
sessionId,
|
|
809
|
-
permissionMode,
|
|
810
|
-
builtinToolFiltering,
|
|
811
|
-
client,
|
|
812
|
-
turn,
|
|
813
|
-
emit,
|
|
814
|
-
}: {
|
|
815
|
-
event: OpenCodeEvent;
|
|
816
|
-
state: TranslationState;
|
|
817
|
-
sessionId: string;
|
|
818
|
-
permissionMode: StartMessage['permissionMode'];
|
|
819
|
-
builtinToolFiltering: StartMessage['builtinToolFiltering'];
|
|
820
|
-
client: OpenCodeClient;
|
|
821
|
-
turn: BridgeTurn;
|
|
822
|
-
emit: Emit;
|
|
823
|
-
}): Promise<void> {
|
|
824
|
-
const type = event.type;
|
|
825
|
-
const props = event.properties ?? {};
|
|
826
|
-
|
|
827
|
-
if (type === 'message.updated') {
|
|
828
|
-
const info = props.info;
|
|
829
|
-
if (isRecord(info)) {
|
|
830
|
-
const id = stringValue(info.id);
|
|
831
|
-
const role = stringValue(info.role);
|
|
832
|
-
if (id && role) state.messageRoles.set(id, role);
|
|
833
|
-
}
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (type === 'message.part.delta') {
|
|
838
|
-
emitLegacyPartDelta({ props, state, emit });
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (type === 'message.part.updated') {
|
|
843
|
-
if (emitLegacyTextPartUpdate({ part: props.part, state, emit })) return;
|
|
844
|
-
if (emitLegacyStepFinishPart({ part: props.part, state, emit })) return;
|
|
845
|
-
emitLegacyToolPart({ part: props.part, state, emit });
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
if (type === 'session.next.text.started') {
|
|
850
|
-
emit({ type: 'text-start', id: String(props.textID ?? event.id) });
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
if (type === 'session.next.text.delta') {
|
|
854
|
-
const id = String(props.textID ?? event.id);
|
|
855
|
-
state.textDeltas.set(
|
|
856
|
-
id,
|
|
857
|
-
`${state.textDeltas.get(id) ?? ''}${String(props.delta ?? '')}`,
|
|
858
|
-
);
|
|
859
|
-
emit({
|
|
860
|
-
type: 'text-delta',
|
|
861
|
-
id,
|
|
862
|
-
delta: String(props.delta ?? ''),
|
|
863
|
-
});
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
if (type === 'session.next.text.ended') {
|
|
867
|
-
const id = String(props.textID ?? event.id);
|
|
868
|
-
emitMissingFinalDelta({
|
|
869
|
-
id,
|
|
870
|
-
fullText: typeof props.text === 'string' ? props.text : undefined,
|
|
871
|
-
emittedText: state.textDeltas.get(id) ?? '',
|
|
872
|
-
emit,
|
|
873
|
-
type: 'text-delta',
|
|
874
|
-
});
|
|
875
|
-
emit({ type: 'text-end', id });
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
if (type === 'session.next.reasoning.started') {
|
|
879
|
-
emit({
|
|
880
|
-
type: 'reasoning-start',
|
|
881
|
-
id: String(props.reasoningID ?? event.id),
|
|
882
|
-
});
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
if (type === 'session.next.reasoning.delta') {
|
|
886
|
-
const id = String(props.reasoningID ?? event.id);
|
|
887
|
-
state.reasoningDeltas.set(
|
|
888
|
-
id,
|
|
889
|
-
`${state.reasoningDeltas.get(id) ?? ''}${String(props.delta ?? '')}`,
|
|
890
|
-
);
|
|
891
|
-
emit({
|
|
892
|
-
type: 'reasoning-delta',
|
|
893
|
-
id,
|
|
894
|
-
delta: String(props.delta ?? ''),
|
|
895
|
-
});
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
if (type === 'session.next.reasoning.ended') {
|
|
899
|
-
const id = String(props.reasoningID ?? event.id);
|
|
900
|
-
emitMissingFinalDelta({
|
|
901
|
-
id,
|
|
902
|
-
fullText: typeof props.text === 'string' ? props.text : undefined,
|
|
903
|
-
emittedText: state.reasoningDeltas.get(id) ?? '',
|
|
904
|
-
emit,
|
|
905
|
-
type: 'reasoning-delta',
|
|
906
|
-
});
|
|
907
|
-
emit({ type: 'reasoning-end', id });
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
if (type === 'session.next.shell.started') {
|
|
911
|
-
const callID = String(props.callID ?? event.id);
|
|
912
|
-
const command = String(props.command ?? '');
|
|
913
|
-
state.shellCommands.set(callID, command);
|
|
914
|
-
emit({
|
|
915
|
-
type: 'tool-call',
|
|
916
|
-
toolCallId: callID,
|
|
917
|
-
toolName: 'bash',
|
|
918
|
-
nativeName: 'bash',
|
|
919
|
-
input: JSON.stringify({ command }),
|
|
920
|
-
providerExecuted: true,
|
|
921
|
-
});
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
if (type === 'session.next.shell.ended') {
|
|
925
|
-
const callID = String(props.callID ?? event.id);
|
|
926
|
-
emit({
|
|
927
|
-
type: 'tool-result',
|
|
928
|
-
toolCallId: callID,
|
|
929
|
-
toolName: 'bash',
|
|
930
|
-
result: {
|
|
931
|
-
command: state.shellCommands.get(callID) ?? '',
|
|
932
|
-
output: String(props.output ?? ''),
|
|
933
|
-
},
|
|
934
|
-
});
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
if (type === 'session.next.tool.input.delta') {
|
|
938
|
-
const callID = String(props.callID ?? event.id);
|
|
939
|
-
state.toolInputs.set(
|
|
940
|
-
callID,
|
|
941
|
-
`${state.toolInputs.get(callID) ?? ''}${String(props.delta ?? '')}`,
|
|
942
|
-
);
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
if (type === 'session.next.tool.input.ended') {
|
|
946
|
-
state.toolInputs.set(
|
|
947
|
-
String(props.callID ?? event.id),
|
|
948
|
-
String(props.text ?? ''),
|
|
949
|
-
);
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
if (type === 'session.next.tool.called') {
|
|
953
|
-
const callID = String(props.callID ?? event.id);
|
|
954
|
-
const rawToolName = String(props.tool ?? 'unknown');
|
|
955
|
-
const toolName = toWireToolName(rawToolName);
|
|
956
|
-
state.toolNames.set(callID, { rawToolName, toolName });
|
|
957
|
-
const hostToolName = getHostToolName(toolName, props.tool);
|
|
958
|
-
if (hostToolName) {
|
|
959
|
-
authorizeHostToolCall({
|
|
960
|
-
callID,
|
|
961
|
-
toolName: hostToolName,
|
|
962
|
-
input: props.input ?? parseToolInput(state, props),
|
|
963
|
-
state,
|
|
787
|
+
if (event.type === 'permission.v2.asked') {
|
|
788
|
+
await handlePermissionV2({
|
|
789
|
+
client,
|
|
790
|
+
sessionId,
|
|
791
|
+
permissionMode,
|
|
792
|
+
builtinToolFiltering,
|
|
793
|
+
turn,
|
|
794
|
+
emit,
|
|
795
|
+
event,
|
|
964
796
|
});
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
...(props.provider?.metadata
|
|
975
|
-
? { providerMetadata: props.provider.metadata }
|
|
976
|
-
: {}),
|
|
977
|
-
});
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
if (
|
|
981
|
-
type === 'session.next.tool.success' ||
|
|
982
|
-
type === 'session.next.tool.failed'
|
|
983
|
-
) {
|
|
984
|
-
const callID = String(props.callID ?? event.id);
|
|
985
|
-
const cachedTool = state.toolNames.get(callID);
|
|
986
|
-
const rawToolName =
|
|
987
|
-
cachedTool?.rawToolName ??
|
|
988
|
-
String((props as { tool?: unknown }).tool ?? '');
|
|
989
|
-
const toolName =
|
|
990
|
-
cachedTool?.toolName ?? toWireToolName(rawToolName || 'unknown');
|
|
991
|
-
if (getHostToolName(toolName, rawToolName)) return;
|
|
992
|
-
emit({
|
|
993
|
-
type: 'tool-result',
|
|
994
|
-
toolCallId: callID,
|
|
995
|
-
toolName,
|
|
996
|
-
result:
|
|
997
|
-
props.result ??
|
|
998
|
-
props.structured ??
|
|
999
|
-
('content' in props ? props.content : null) ??
|
|
1000
|
-
null,
|
|
1001
|
-
...(type === 'session.next.tool.failed' ? { isError: true } : {}),
|
|
1002
|
-
});
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
if (type === 'session.next.retried') {
|
|
1006
|
-
const error = props.error ?? event;
|
|
1007
|
-
if (isRecord(error) && error.isRetryable === false) {
|
|
1008
|
-
turn.emitError({
|
|
1009
|
-
error,
|
|
1010
|
-
message: 'OpenCode session retry failed',
|
|
797
|
+
} else if (event.type === 'permission.asked') {
|
|
798
|
+
await handlePermission({
|
|
799
|
+
client,
|
|
800
|
+
sessionId,
|
|
801
|
+
permissionMode,
|
|
802
|
+
builtinToolFiltering,
|
|
803
|
+
turn,
|
|
804
|
+
emit,
|
|
805
|
+
event,
|
|
1011
806
|
});
|
|
1012
807
|
} else {
|
|
1013
|
-
|
|
808
|
+
emitStreamEvent(event);
|
|
1014
809
|
}
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1017
|
-
if (type === 'session.next.step.ended') {
|
|
1018
|
-
closeLegacyOpenParts({ state, emit });
|
|
1019
|
-
state.turnUsage = mapUsage(props.tokens);
|
|
1020
|
-
emit({
|
|
1021
|
-
type: 'finish-step',
|
|
1022
|
-
finishReason: {
|
|
1023
|
-
unified: mapOpenCodeFinishReason(String(props.finish ?? 'stop')),
|
|
1024
|
-
raw: String(props.finish ?? 'stop'),
|
|
1025
|
-
},
|
|
1026
|
-
usage: state.turnUsage,
|
|
1027
|
-
...(typeof props.cost === 'number'
|
|
1028
|
-
? { harnessMetadata: { opencode: { cost: props.cost } } }
|
|
1029
|
-
: {}),
|
|
1030
|
-
});
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
if (type === 'session.next.compaction.ended') {
|
|
1034
|
-
emit({
|
|
1035
|
-
type: 'compaction',
|
|
1036
|
-
trigger: props.reason === 'auto' ? 'auto' : 'manual',
|
|
1037
|
-
summary: String(props.text ?? ''),
|
|
1038
|
-
harnessMetadata: {
|
|
1039
|
-
opencode: {
|
|
1040
|
-
recent: String(props.recent ?? ''),
|
|
1041
|
-
},
|
|
1042
|
-
},
|
|
1043
|
-
});
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
if (type === 'file.edited') {
|
|
1047
|
-
emit({
|
|
1048
|
-
type: 'file-change',
|
|
1049
|
-
event: 'modify',
|
|
1050
|
-
path: stripWorkDir(String(props.file ?? '')),
|
|
1051
|
-
});
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
if (type === 'session.error' || type === 'session.next.step.failed') {
|
|
1055
|
-
const error = props.error ?? event;
|
|
1056
|
-
turn.emitError({
|
|
1057
|
-
error,
|
|
1058
|
-
message:
|
|
1059
|
-
type === 'session.error'
|
|
1060
|
-
? 'OpenCode session error'
|
|
1061
|
-
: 'OpenCode step failed',
|
|
1062
|
-
});
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
if (type === 'permission.v2.asked') {
|
|
1066
|
-
await handlePermissionV2({
|
|
1067
|
-
client,
|
|
1068
|
-
sessionId,
|
|
1069
|
-
permissionMode,
|
|
1070
|
-
builtinToolFiltering,
|
|
1071
|
-
turn,
|
|
1072
|
-
emit,
|
|
1073
|
-
event,
|
|
1074
|
-
});
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
if (type === 'permission.asked') {
|
|
1078
|
-
await handlePermission({
|
|
1079
|
-
client,
|
|
1080
|
-
sessionId,
|
|
1081
|
-
permissionMode,
|
|
1082
|
-
builtinToolFiltering,
|
|
1083
|
-
turn,
|
|
1084
|
-
emit,
|
|
1085
|
-
event,
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function closeLegacyOpenParts({
|
|
1091
|
-
state,
|
|
1092
|
-
emit,
|
|
1093
|
-
}: {
|
|
1094
|
-
state: TranslationState;
|
|
1095
|
-
emit: Emit;
|
|
1096
|
-
}): void {
|
|
1097
|
-
for (const id of state.legacyReasoningPartIds) {
|
|
1098
|
-
emit({ type: 'reasoning-end', id });
|
|
1099
|
-
state.reasoningDeltas.delete(id);
|
|
1100
|
-
}
|
|
1101
|
-
state.legacyReasoningPartIds.clear();
|
|
1102
|
-
for (const id of state.legacyTextPartIds) {
|
|
1103
|
-
emit({ type: 'text-end', id });
|
|
1104
|
-
state.textDeltas.delete(id);
|
|
1105
|
-
}
|
|
1106
|
-
state.legacyTextPartIds.clear();
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function emitLegacyStepFinishPart({
|
|
1110
|
-
part,
|
|
1111
|
-
state,
|
|
1112
|
-
emit,
|
|
1113
|
-
}: {
|
|
1114
|
-
part: unknown;
|
|
1115
|
-
state: TranslationState;
|
|
1116
|
-
emit: Emit;
|
|
1117
|
-
}): boolean {
|
|
1118
|
-
const event = legacyStepFinishPartToFinishStep(part);
|
|
1119
|
-
if (!event) return false;
|
|
1120
|
-
const id = isRecord(part) ? stringValue(part.id) : undefined;
|
|
1121
|
-
if (id) {
|
|
1122
|
-
if (state.legacyStepFinishPartIds.has(id)) return true;
|
|
1123
|
-
state.legacyStepFinishPartIds.add(id);
|
|
1124
|
-
}
|
|
1125
|
-
closeLegacyOpenParts({ state, emit });
|
|
1126
|
-
state.turnUsage = event.usage as Record<string, unknown>;
|
|
1127
|
-
emit(event);
|
|
1128
|
-
return true;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function emitLegacyToolPart({
|
|
1132
|
-
part,
|
|
1133
|
-
state,
|
|
1134
|
-
emit,
|
|
1135
|
-
}: {
|
|
1136
|
-
part: unknown;
|
|
1137
|
-
state: TranslationState;
|
|
1138
|
-
emit: Emit;
|
|
1139
|
-
}): void {
|
|
1140
|
-
if (!part || typeof part !== 'object') return;
|
|
1141
|
-
const toolPart = part as Record<string, any>;
|
|
1142
|
-
if (toolPart.type !== 'tool') return;
|
|
1143
|
-
const status = legacyToolPartStatus(toolPart);
|
|
1144
|
-
if (status !== 'running' && status !== 'completed' && status !== 'error') {
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
if (
|
|
1148
|
-
typeof toolPart.tool !== 'string' ||
|
|
1149
|
-
typeof toolPart.callID !== 'string'
|
|
1150
|
-
) {
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
const callID = toolPart.callID;
|
|
1154
|
-
const rawToolName = toolPart.tool;
|
|
1155
|
-
const toolName = toWireToolName(rawToolName);
|
|
1156
|
-
state.toolNames.set(callID, { rawToolName, toolName });
|
|
1157
|
-
const hostToolName = getHostToolName(toolName, rawToolName);
|
|
1158
|
-
if (hostToolName) {
|
|
1159
|
-
if (status === 'running') {
|
|
1160
|
-
authorizeHostToolCall({
|
|
1161
|
-
callID,
|
|
1162
|
-
toolName: hostToolName,
|
|
1163
|
-
input: legacyToolPartInput(toolPart),
|
|
1164
|
-
state,
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
if (!state.toolCallsEmitted.has(callID)) {
|
|
1170
|
-
state.toolCallsEmitted.add(callID);
|
|
1171
|
-
emit({
|
|
1172
|
-
type: 'tool-call',
|
|
1173
|
-
toolCallId: callID,
|
|
1174
|
-
toolName,
|
|
1175
|
-
...nativeNameField({ nativeName: rawToolName, toolName }),
|
|
1176
|
-
input: JSON.stringify(legacyToolPartInput(toolPart)),
|
|
1177
|
-
providerExecuted: true,
|
|
1178
|
-
...(toolPart.provider?.metadata
|
|
1179
|
-
? { providerMetadata: toolPart.provider.metadata }
|
|
1180
|
-
: {}),
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
if (
|
|
1184
|
-
(status === 'completed' || status === 'error') &&
|
|
1185
|
-
!state.toolResultsEmitted.has(callID)
|
|
1186
|
-
) {
|
|
1187
|
-
state.toolResultsEmitted.add(callID);
|
|
1188
|
-
emit({
|
|
1189
|
-
type: 'tool-result',
|
|
1190
|
-
toolCallId: callID,
|
|
1191
|
-
toolName,
|
|
1192
|
-
result: legacyToolPartOutput(toolPart),
|
|
1193
|
-
...(status === 'error' ? { isError: true } : {}),
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
function legacyToolPartStatus(part: Record<string, any>): string | undefined {
|
|
1199
|
-
return typeof part.state === 'string'
|
|
1200
|
-
? part.state
|
|
1201
|
-
: typeof part.state === 'object' && part.state !== null
|
|
1202
|
-
? String(part.state.status ?? '')
|
|
1203
|
-
: undefined;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
function legacyToolPartInput(
|
|
1207
|
-
part: Record<string, any>,
|
|
1208
|
-
): Record<string, unknown> {
|
|
1209
|
-
const state =
|
|
1210
|
-
typeof part.state === 'object' && part.state !== null
|
|
1211
|
-
? (part.state as Record<string, any>)
|
|
1212
|
-
: undefined;
|
|
1213
|
-
return {
|
|
1214
|
-
...(isRecord(part.metadata) ? part.metadata : {}),
|
|
1215
|
-
...(isRecord(state?.metadata) ? state.metadata : {}),
|
|
1216
|
-
...(isRecord(state?.input) ? state.input : {}),
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
function legacyToolPartOutput(part: Record<string, any>): unknown {
|
|
1221
|
-
const state =
|
|
1222
|
-
typeof part.state === 'object' && part.state !== null
|
|
1223
|
-
? (part.state as Record<string, any>)
|
|
1224
|
-
: undefined;
|
|
1225
|
-
if (state?.status === 'error') {
|
|
1226
|
-
return state.error ?? part.error ?? state.result ?? 'tool failed';
|
|
1227
|
-
}
|
|
1228
|
-
return (
|
|
1229
|
-
state?.output ??
|
|
1230
|
-
state?.result ??
|
|
1231
|
-
state?.structured ??
|
|
1232
|
-
state?.content ??
|
|
1233
|
-
null
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1238
|
-
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function parseToolInput(
|
|
1242
|
-
state: TranslationState,
|
|
1243
|
-
props: Record<string, any>,
|
|
1244
|
-
): unknown {
|
|
1245
|
-
const text = state.toolInputs.get(String(props.callID ?? ''));
|
|
1246
|
-
if (!text) return {};
|
|
1247
|
-
try {
|
|
1248
|
-
return JSON.parse(text);
|
|
1249
|
-
} catch {
|
|
1250
|
-
return { input: text };
|
|
810
|
+
if (onEvent?.(event)) break;
|
|
1251
811
|
}
|
|
1252
812
|
}
|
|
1253
813
|
|
|
@@ -1509,16 +1069,19 @@ function authorizeHostToolCall({
|
|
|
1509
1069
|
async function emitContextFallback({
|
|
1510
1070
|
client,
|
|
1511
1071
|
sessionId,
|
|
1072
|
+
state,
|
|
1512
1073
|
emit,
|
|
1513
1074
|
emitContent,
|
|
1514
1075
|
}: {
|
|
1515
1076
|
client: OpenCodeClient;
|
|
1516
1077
|
sessionId: string;
|
|
1078
|
+
state: TranslationState;
|
|
1517
1079
|
emit: Emit;
|
|
1518
1080
|
emitContent: boolean;
|
|
1519
1081
|
}): Promise<boolean> {
|
|
1520
1082
|
const assistant = await latestAssistantSnapshot({ client, sessionId });
|
|
1521
1083
|
if (!assistant) return false;
|
|
1084
|
+
emitOpenCodeStreamStart({ info: assistant, state, emit });
|
|
1522
1085
|
if (emitContent && Array.isArray(assistant.contentParts)) {
|
|
1523
1086
|
for (const part of assistant.contentParts) {
|
|
1524
1087
|
emitAssistantContentPart(part, emit);
|
|
@@ -1771,10 +1334,6 @@ function modelRefFromValue(value: unknown): OpenCodeModelRef | undefined {
|
|
|
1771
1334
|
return { providerID, modelID };
|
|
1772
1335
|
}
|
|
1773
1336
|
|
|
1774
|
-
function stringValue(value: unknown): string | undefined {
|
|
1775
|
-
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
1337
|
function stripWorkDir(file: string): string {
|
|
1779
1338
|
if (!file) return file;
|
|
1780
1339
|
const normalized = path.resolve(file);
|
|
@@ -7,6 +7,7 @@ export type OpenCodeEvent = {
|
|
|
7
7
|
type Emit = (msg: Record<string, unknown>) => void;
|
|
8
8
|
|
|
9
9
|
export type TranslationState = {
|
|
10
|
+
streamStarted: boolean;
|
|
10
11
|
textDeltas: Map<string, string>;
|
|
11
12
|
reasoningDeltas: Map<string, string>;
|
|
12
13
|
toolInputs: Map<string, string>;
|
|
@@ -24,6 +25,7 @@ export type TranslationState = {
|
|
|
24
25
|
|
|
25
26
|
export function createTranslationState(): TranslationState {
|
|
26
27
|
return {
|
|
28
|
+
streamStarted: false,
|
|
27
29
|
textDeltas: new Map(),
|
|
28
30
|
reasoningDeltas: new Map(),
|
|
29
31
|
toolInputs: new Map(),
|
|
@@ -40,6 +42,26 @@ export function createTranslationState(): TranslationState {
|
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
export function emitOpenCodeStreamStart({
|
|
46
|
+
info,
|
|
47
|
+
state,
|
|
48
|
+
emit,
|
|
49
|
+
}: {
|
|
50
|
+
info: unknown;
|
|
51
|
+
state: TranslationState;
|
|
52
|
+
emit: Emit;
|
|
53
|
+
}): void {
|
|
54
|
+
if (state.streamStarted || !isRecord(info)) return;
|
|
55
|
+
if (info.role !== 'assistant' && info.type !== 'assistant') return;
|
|
56
|
+
const providerID = stringValue(info.providerID);
|
|
57
|
+
const modelID = stringValue(info.modelID);
|
|
58
|
+
const modelId =
|
|
59
|
+
providerID && modelID ? `${providerID}/${modelID}` : undefined;
|
|
60
|
+
|
|
61
|
+
state.streamStarted = true;
|
|
62
|
+
emit({ type: 'stream-start', ...(modelId ? { modelId } : {}) });
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
export function unwrapOpenCodeEvent(
|
|
44
66
|
rawEvent: unknown,
|
|
45
67
|
): OpenCodeEvent | undefined {
|