@raindrop-ai/opencode-plugin 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +576 -300
- package/dist/index.js +576 -300
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -49,12 +49,72 @@ function loadConfig(projectDirectory) {
|
|
|
49
49
|
writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
|
|
50
50
|
endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
|
|
51
51
|
eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
|
|
52
|
-
userId: merged.user_id,
|
|
53
52
|
debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false
|
|
54
53
|
};
|
|
55
54
|
}
|
|
56
55
|
|
|
56
|
+
// package.json
|
|
57
|
+
var package_default = {
|
|
58
|
+
name: "@raindrop-ai/opencode-plugin",
|
|
59
|
+
version: "0.0.4",
|
|
60
|
+
description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
|
|
61
|
+
type: "module",
|
|
62
|
+
main: "dist/index.cjs",
|
|
63
|
+
module: "dist/index.js",
|
|
64
|
+
types: "dist/index.d.ts",
|
|
65
|
+
exports: {
|
|
66
|
+
".": {
|
|
67
|
+
types: "./dist/index.d.ts",
|
|
68
|
+
import: "./dist/index.js",
|
|
69
|
+
require: "./dist/index.cjs"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
sideEffects: false,
|
|
73
|
+
files: [
|
|
74
|
+
"dist/**"
|
|
75
|
+
],
|
|
76
|
+
scripts: {
|
|
77
|
+
build: "tsup",
|
|
78
|
+
dev: "tsup --watch",
|
|
79
|
+
clean: "rm -rf dist",
|
|
80
|
+
test: "cd tests && yarn test"
|
|
81
|
+
},
|
|
82
|
+
peerDependencies: {
|
|
83
|
+
"@opencode-ai/plugin": ">=1.2.0",
|
|
84
|
+
"@opencode-ai/sdk": ">=1.2.0"
|
|
85
|
+
},
|
|
86
|
+
peerDependenciesMeta: {
|
|
87
|
+
"@opencode-ai/sdk": {
|
|
88
|
+
optional: true
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
devDependencies: {
|
|
92
|
+
"@opencode-ai/plugin": "^1.2.0",
|
|
93
|
+
"@opencode-ai/sdk": "^1.2.0",
|
|
94
|
+
"@types/node": "^20.11.17",
|
|
95
|
+
tsup: "^8.4.0",
|
|
96
|
+
typescript: "^5.3.3"
|
|
97
|
+
},
|
|
98
|
+
tsup: {
|
|
99
|
+
entry: [
|
|
100
|
+
"src/index.ts"
|
|
101
|
+
],
|
|
102
|
+
format: [
|
|
103
|
+
"cjs",
|
|
104
|
+
"esm"
|
|
105
|
+
],
|
|
106
|
+
dts: {
|
|
107
|
+
resolve: true
|
|
108
|
+
},
|
|
109
|
+
clean: true
|
|
110
|
+
},
|
|
111
|
+
publishConfig: {
|
|
112
|
+
access: "public"
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
57
116
|
// src/shipper.ts
|
|
117
|
+
var PLUGIN_VERSION = package_default.version;
|
|
58
118
|
function getCryptoObj() {
|
|
59
119
|
return globalThis.crypto;
|
|
60
120
|
}
|
|
@@ -94,10 +154,7 @@ function base64Encode(bytes) {
|
|
|
94
154
|
return out;
|
|
95
155
|
}
|
|
96
156
|
var SERVICE_NAME = "raindrop.opencode-plugin";
|
|
97
|
-
var PLUGIN_VERSION = "0.1.0";
|
|
98
157
|
var SpanStatusCode = {
|
|
99
|
-
UNSET: 0,
|
|
100
|
-
OK: 1,
|
|
101
158
|
ERROR: 2
|
|
102
159
|
};
|
|
103
160
|
function generateId() {
|
|
@@ -113,7 +170,7 @@ function createSpanIds(parent) {
|
|
|
113
170
|
};
|
|
114
171
|
}
|
|
115
172
|
function nowUnixNanoString() {
|
|
116
|
-
return
|
|
173
|
+
return Date.now().toString() + "000000";
|
|
117
174
|
}
|
|
118
175
|
function attrString(key, value) {
|
|
119
176
|
if (value === void 0) return void 0;
|
|
@@ -125,7 +182,11 @@ function attrInt(key, value) {
|
|
|
125
182
|
return { key, value: { intValue: String(Math.trunc(value)) } };
|
|
126
183
|
}
|
|
127
184
|
function buildOtlpSpan(args) {
|
|
128
|
-
const attrs =
|
|
185
|
+
const attrs = [
|
|
186
|
+
attrString("span.id", args.ids.spanIdB64),
|
|
187
|
+
attrString("span.parent.id", args.ids.parentSpanIdB64),
|
|
188
|
+
...args.attributes
|
|
189
|
+
].filter((x) => x !== void 0);
|
|
129
190
|
const span = {
|
|
130
191
|
traceId: args.ids.traceIdB64,
|
|
131
192
|
spanId: args.ids.spanIdB64,
|
|
@@ -195,9 +256,7 @@ async function withRetry(operation, opName, opts) {
|
|
|
195
256
|
}
|
|
196
257
|
if (delay > 0) await wait(delay);
|
|
197
258
|
} else if (opts.debug) {
|
|
198
|
-
console.log(
|
|
199
|
-
`[raindrop-ai/opencode-plugin] ${opName} attempt ${attempt}/${opts.maxAttempts}`
|
|
200
|
-
);
|
|
259
|
+
console.log(`[raindrop-ai/opencode-plugin] ${opName} attempt ${attempt}/${opts.maxAttempts}`);
|
|
201
260
|
}
|
|
202
261
|
try {
|
|
203
262
|
return await operation();
|
|
@@ -477,10 +536,12 @@ var TraceShipper = class {
|
|
|
477
536
|
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
478
537
|
const body = buildExportTraceServiceRequest(batch);
|
|
479
538
|
const url = `${this.baseUrl}traces`;
|
|
539
|
+
const spanNames = batch.map((span) => span.name);
|
|
480
540
|
if (this.debug) {
|
|
481
541
|
console.log("[raindrop-ai/opencode-plugin] sending traces batch", {
|
|
482
542
|
spans: batch.length,
|
|
483
|
-
endpoint: url
|
|
543
|
+
endpoint: url,
|
|
544
|
+
spanNames
|
|
484
545
|
});
|
|
485
546
|
}
|
|
486
547
|
const p = postJson(
|
|
@@ -494,14 +555,14 @@ var TraceShipper = class {
|
|
|
494
555
|
try {
|
|
495
556
|
await p;
|
|
496
557
|
if (this.debug) {
|
|
497
|
-
console.log(`[raindrop-ai/opencode-plugin] sent ${batch.length} spans
|
|
558
|
+
console.log(`[raindrop-ai/opencode-plugin] sent ${batch.length} spans`, {
|
|
559
|
+
spanNames
|
|
560
|
+
});
|
|
498
561
|
}
|
|
499
562
|
} catch (err) {
|
|
500
563
|
if (this.debug) {
|
|
501
564
|
const msg = err instanceof Error ? err.message : String(err);
|
|
502
|
-
console.warn(
|
|
503
|
-
`[raindrop-ai/opencode-plugin] failed to send spans (dropping): ${msg}`
|
|
504
|
-
);
|
|
565
|
+
console.warn(`[raindrop-ai/opencode-plugin] failed to send spans (dropping): ${msg}`);
|
|
505
566
|
}
|
|
506
567
|
}
|
|
507
568
|
} finally {
|
|
@@ -520,328 +581,458 @@ var TraceShipper = class {
|
|
|
520
581
|
}
|
|
521
582
|
};
|
|
522
583
|
|
|
584
|
+
// src/tracing-linkage-helpers.ts
|
|
585
|
+
function createSessionParentMapHelpers({
|
|
586
|
+
sessions: sessions2,
|
|
587
|
+
taskContexts: taskContexts2,
|
|
588
|
+
runningTaskCallsBySession: runningTaskCallsBySession2,
|
|
589
|
+
mapChildSessionToParent: mapChildSessionToParent2,
|
|
590
|
+
pendingChildSessionsByCallKey: pendingChildSessionsByCallKey2,
|
|
591
|
+
callKey: callKey2,
|
|
592
|
+
log
|
|
593
|
+
}) {
|
|
594
|
+
function applyChildSessionParentToState(childSessionId) {
|
|
595
|
+
const parentInfo = mapChildSessionToParent2.get(childSessionId);
|
|
596
|
+
const state = sessions2.get(childSessionId);
|
|
597
|
+
if (!parentInfo || !state) return;
|
|
598
|
+
state.parentId = parentInfo.parentId;
|
|
599
|
+
state.parentTaskSpanIds = parentInfo.parentTaskSpanIds;
|
|
600
|
+
state.parentEventContext = parentInfo.eventContext;
|
|
601
|
+
}
|
|
602
|
+
function queuePendingChildByCallKey(key, childSessionId) {
|
|
603
|
+
var _a;
|
|
604
|
+
const pending = (_a = pendingChildSessionsByCallKey2.get(key)) != null ? _a : /* @__PURE__ */ new Set();
|
|
605
|
+
pending.add(childSessionId);
|
|
606
|
+
pendingChildSessionsByCallKey2.set(key, pending);
|
|
607
|
+
}
|
|
608
|
+
function attachChildSessionToParentTask(childSessionId, parentId, parentCallId, source) {
|
|
609
|
+
const key = callKey2(parentId, parentCallId);
|
|
610
|
+
const ctx = taskContexts2.get(key);
|
|
611
|
+
if (!ctx) {
|
|
612
|
+
queuePendingChildByCallKey(key, childSessionId);
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
const parentInfo = {
|
|
616
|
+
parentId,
|
|
617
|
+
parentTaskSpanIds: ctx.ids,
|
|
618
|
+
eventContext: ctx.eventContext
|
|
619
|
+
};
|
|
620
|
+
mapChildSessionToParent2.set(childSessionId, parentInfo);
|
|
621
|
+
applyChildSessionParentToState(childSessionId);
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
function resolvePendingChildrenForCall(parentId, parentCallId, source) {
|
|
625
|
+
const key = callKey2(parentId, parentCallId);
|
|
626
|
+
const pending = pendingChildSessionsByCallKey2.get(key);
|
|
627
|
+
if (pending && pending.size > 0) {
|
|
628
|
+
for (const childSessionId of [...pending]) {
|
|
629
|
+
if (attachChildSessionToParentTask(childSessionId, parentId, parentCallId, source)) {
|
|
630
|
+
pending.delete(childSessionId);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (pending.size === 0) pendingChildSessionsByCallKey2.delete(key);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function cleanupSessionParentMap(sessionId) {
|
|
637
|
+
mapChildSessionToParent2.delete(sessionId);
|
|
638
|
+
runningTaskCallsBySession2.delete(sessionId);
|
|
639
|
+
for (const [key, pending] of pendingChildSessionsByCallKey2.entries()) {
|
|
640
|
+
pending.delete(sessionId);
|
|
641
|
+
if (pending.size === 0) pendingChildSessionsByCallKey2.delete(key);
|
|
642
|
+
}
|
|
643
|
+
for (const key of [...taskContexts2.keys()]) {
|
|
644
|
+
if (key.startsWith(`${sessionId}:`)) taskContexts2.delete(key);
|
|
645
|
+
}
|
|
646
|
+
for (const [childSessionId, parentInfo] of [...mapChildSessionToParent2.entries()]) {
|
|
647
|
+
if (parentInfo.parentId === sessionId) {
|
|
648
|
+
mapChildSessionToParent2.delete(childSessionId);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
applyChildSessionParentToState,
|
|
654
|
+
attachChildSessionToParentTask,
|
|
655
|
+
cleanupSessionParentMap,
|
|
656
|
+
resolvePendingChildrenForCall
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
523
660
|
// src/tracing.ts
|
|
524
661
|
var sessions = /* @__PURE__ */ new Map();
|
|
662
|
+
var taskContexts = /* @__PURE__ */ new Map();
|
|
663
|
+
var runningTaskCallsBySession = /* @__PURE__ */ new Map();
|
|
664
|
+
var mapChildSessionToParent = /* @__PURE__ */ new Map();
|
|
665
|
+
var pendingChildSessionsByCallKey = /* @__PURE__ */ new Map();
|
|
666
|
+
function createSessionState(sessionId) {
|
|
667
|
+
return {
|
|
668
|
+
sessionId,
|
|
669
|
+
turnNumber: 0,
|
|
670
|
+
toolCount: 0,
|
|
671
|
+
currentInput: "",
|
|
672
|
+
outputParts: /* @__PURE__ */ new Map(),
|
|
673
|
+
reasoningParts: /* @__PURE__ */ new Map(),
|
|
674
|
+
toolSpanStarts: /* @__PURE__ */ new Map(),
|
|
675
|
+
processedMessages: /* @__PURE__ */ new Set()
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function callKey(sessionId, callId) {
|
|
679
|
+
return `${sessionId}:${callId}`;
|
|
680
|
+
}
|
|
525
681
|
function getHostname() {
|
|
526
|
-
var _a
|
|
682
|
+
var _a;
|
|
527
683
|
try {
|
|
528
684
|
const bunGlobal = globalThis;
|
|
529
685
|
if ((_a = bunGlobal.Bun) == null ? void 0 : _a.hostname) return bunGlobal.Bun.hostname;
|
|
530
686
|
return require("os").hostname();
|
|
531
687
|
} catch (e) {
|
|
532
|
-
|
|
688
|
+
const h = process.env["HOSTNAME"];
|
|
689
|
+
if (h === void 0 || h === "") throw new Error("HOSTNAME env is not set");
|
|
690
|
+
return h;
|
|
533
691
|
}
|
|
534
692
|
}
|
|
535
|
-
function
|
|
536
|
-
var _a;
|
|
537
|
-
const parts = worktree.split("/").filter(Boolean);
|
|
538
|
-
return (_a = parts[parts.length - 1]) != null ? _a : "unknown";
|
|
539
|
-
}
|
|
540
|
-
function createHooks(client, config, worktree, directory, eventShipper, traceShipper) {
|
|
693
|
+
function createHooks(config, worktree, directory, eventShipper, traceShipper) {
|
|
541
694
|
function log(msg, data) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
})) == null ? void 0 : _a.catch) == null ? void 0 : _b.call(_a, () => {
|
|
551
|
-
});
|
|
695
|
+
if (!config.debug) return;
|
|
696
|
+
const prefix = `[raindrop-ai/opencode-plugin] [info] ${msg}`;
|
|
697
|
+
if (data !== void 0) {
|
|
698
|
+
console.log(prefix, data);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
console.log(prefix);
|
|
552
702
|
}
|
|
553
703
|
const hostname = getHostname();
|
|
554
704
|
const os = process.platform;
|
|
555
|
-
|
|
705
|
+
const {
|
|
706
|
+
applyChildSessionParentToState,
|
|
707
|
+
attachChildSessionToParentTask,
|
|
708
|
+
cleanupSessionParentMap,
|
|
709
|
+
resolvePendingChildrenForCall
|
|
710
|
+
} = createSessionParentMapHelpers({
|
|
711
|
+
sessions,
|
|
712
|
+
taskContexts,
|
|
713
|
+
runningTaskCallsBySession,
|
|
714
|
+
mapChildSessionToParent,
|
|
715
|
+
pendingChildSessionsByCallKey,
|
|
716
|
+
callKey,
|
|
717
|
+
log
|
|
718
|
+
});
|
|
556
719
|
return {
|
|
557
720
|
// ------------------------------------------------------------------
|
|
558
721
|
// event — session lifecycle + streaming parts
|
|
559
722
|
// ------------------------------------------------------------------
|
|
560
|
-
event: async ({
|
|
561
|
-
|
|
562
|
-
}) => {
|
|
563
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
723
|
+
event: async ({ event }) => {
|
|
724
|
+
var _a, _b, _c, _d, _e;
|
|
564
725
|
try {
|
|
565
726
|
const props = event.properties;
|
|
566
727
|
const info = props["info"];
|
|
567
|
-
const sessionID = props["sessionID"]
|
|
728
|
+
const sessionID = (_b = (_a = props["sessionID"]) != null ? _a : info == null ? void 0 : info["id"]) != null ? _b : props["id"];
|
|
729
|
+
const sessionEventTypes = ["session.created", "session.idle", "session.deleted", "session.error"];
|
|
730
|
+
if (sessionEventTypes.includes(event.type) && (sessionID === void 0 || sessionID === "")) {
|
|
731
|
+
throw new Error(`event ${event.type}: missing sessionID in properties`);
|
|
732
|
+
}
|
|
568
733
|
if (event.type === "session.created") {
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
734
|
+
if (info == null || typeof info !== "object")
|
|
735
|
+
throw new Error("session.created: props.info is required");
|
|
736
|
+
const parentID = info["parentID"];
|
|
737
|
+
let state = sessions.get(sessionID);
|
|
738
|
+
if (!state) {
|
|
739
|
+
state = createSessionState(sessionID);
|
|
740
|
+
sessions.set(sessionID, state);
|
|
572
741
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
sessionId: sessionID,
|
|
576
|
-
userId,
|
|
577
|
-
turnNumber: 0,
|
|
578
|
-
toolCount: 0,
|
|
579
|
-
startTime: Date.now(),
|
|
580
|
-
currentInput: "",
|
|
581
|
-
currentOutput: "",
|
|
582
|
-
outputParts: /* @__PURE__ */ new Map(),
|
|
583
|
-
reasoningParts: /* @__PURE__ */ new Map(),
|
|
584
|
-
toolCalls: /* @__PURE__ */ new Map(),
|
|
585
|
-
toolStartTimes: /* @__PURE__ */ new Map(),
|
|
586
|
-
toolSpanStarts: /* @__PURE__ */ new Map(),
|
|
587
|
-
toolCallMessageIds: /* @__PURE__ */ new Map(),
|
|
588
|
-
processedMessages: /* @__PURE__ */ new Set(),
|
|
589
|
-
attachments: []
|
|
590
|
-
};
|
|
591
|
-
state.currentRootSpan = traceShipper.startSpan({
|
|
592
|
-
name: `OpenCode: ${getProjectName(worktree)}`,
|
|
593
|
-
eventId: sessionID,
|
|
594
|
-
attributes: [
|
|
595
|
-
attrString("workspace", worktree),
|
|
596
|
-
attrString("directory", directory),
|
|
597
|
-
attrString("hostname", hostname),
|
|
598
|
-
attrString("os", os)
|
|
599
|
-
]
|
|
600
|
-
});
|
|
601
|
-
sessions.set(sessionID, state);
|
|
602
|
-
log("Session created", { sessionID, userId });
|
|
742
|
+
state.parentId = parentID;
|
|
743
|
+
if (state.parentId) mapChildSessionToParent.set(sessionID, { parentId: state.parentId });
|
|
603
744
|
} else if (event.type === "message.part.updated") {
|
|
604
745
|
const part = props["part"];
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
|
|
746
|
+
if (part == null || typeof part !== "object")
|
|
747
|
+
throw new Error("message.part.updated: props.part is required");
|
|
748
|
+
const partObj = part;
|
|
749
|
+
const partSessionID = partObj["sessionID"];
|
|
750
|
+
const messageId = partObj["messageID"];
|
|
751
|
+
if (partSessionID == null || partSessionID === "")
|
|
752
|
+
throw new Error("message.part.updated: part.sessionID is required");
|
|
753
|
+
if (messageId == null || messageId === "")
|
|
754
|
+
throw new Error("message.part.updated: part.messageID is required");
|
|
609
755
|
const state = sessions.get(partSessionID);
|
|
610
756
|
if (!state) return;
|
|
611
|
-
if (
|
|
612
|
-
state.outputParts.set(messageId,
|
|
613
|
-
} else if (
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
|
|
757
|
+
if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
|
|
758
|
+
state.outputParts.set(messageId, partObj["text"]);
|
|
759
|
+
} else if (partObj["type"] === "tool" && messageId) {
|
|
760
|
+
const tool = partObj["tool"];
|
|
761
|
+
if (tool === "task") {
|
|
762
|
+
const callID = partObj["callID"];
|
|
763
|
+
const partState = partObj["state"];
|
|
764
|
+
if (callID == null || callID === "")
|
|
765
|
+
throw new Error("message.part.updated: part.callID is required for tool part");
|
|
766
|
+
if (partState == null || typeof partState !== "object")
|
|
767
|
+
throw new Error("message.part.updated: part.state is required for tool part");
|
|
768
|
+
const metadata = partState["metadata"];
|
|
769
|
+
const childSessionId = metadata != null && typeof metadata === "object" ? metadata["sessionId"] : void 0;
|
|
770
|
+
if (childSessionId != null && childSessionId !== "") {
|
|
771
|
+
mapChildSessionToParent.set(childSessionId, {
|
|
772
|
+
parentId: partSessionID
|
|
773
|
+
});
|
|
774
|
+
attachChildSessionToParentTask(
|
|
775
|
+
childSessionId,
|
|
776
|
+
partSessionID,
|
|
777
|
+
callID,
|
|
778
|
+
"part.metadata"
|
|
779
|
+
);
|
|
623
780
|
}
|
|
624
|
-
const existingIdx = calls.findIndex((c) => c.callId === callID);
|
|
625
|
-
const entry = { callId: callID, name: tool, args: JSON.stringify(input) };
|
|
626
|
-
if (existingIdx >= 0) calls[existingIdx] = entry;
|
|
627
|
-
else calls.push(entry);
|
|
628
|
-
state.toolCallMessageIds.set(callID, messageId);
|
|
629
781
|
}
|
|
630
|
-
} else if (
|
|
631
|
-
state.reasoningParts.set(messageId,
|
|
632
|
-
} else {
|
|
633
|
-
log(`message.part.updated: skipping part type=${String(part["type"])}`);
|
|
782
|
+
} else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
|
|
783
|
+
state.reasoningParts.set(messageId, partObj["text"]);
|
|
634
784
|
}
|
|
635
785
|
} else if (event.type === "message.updated") {
|
|
636
786
|
const msgInfo = props["info"];
|
|
637
|
-
if (
|
|
638
|
-
|
|
787
|
+
if (msgInfo == null || typeof msgInfo !== "object")
|
|
788
|
+
throw new Error("message.updated: props.info is required");
|
|
789
|
+
const info2 = msgInfo;
|
|
790
|
+
const role = info2["role"];
|
|
639
791
|
if (role !== "assistant") return;
|
|
640
|
-
const msgSessionID =
|
|
641
|
-
const messageId =
|
|
642
|
-
const time =
|
|
643
|
-
if (
|
|
792
|
+
const msgSessionID = info2["sessionID"];
|
|
793
|
+
const messageId = info2["id"];
|
|
794
|
+
const time = info2["time"];
|
|
795
|
+
if (msgSessionID == null || msgSessionID === "")
|
|
796
|
+
throw new Error("message.updated: info.sessionID is required");
|
|
797
|
+
if (messageId == null || messageId === "")
|
|
798
|
+
throw new Error("message.updated: info.id is required");
|
|
799
|
+
if (time == null || typeof time !== "object")
|
|
800
|
+
throw new Error("message.updated: info.time is required");
|
|
801
|
+
const timeObj = time;
|
|
802
|
+
if (timeObj["completed"] == null) return;
|
|
644
803
|
const state = sessions.get(msgSessionID);
|
|
645
|
-
if (!state || !state.currentEventId || !state.
|
|
804
|
+
if (!state || !state.currentEventId || !state.currentRootSpan) return;
|
|
646
805
|
if (state.processedMessages.has(messageId)) return;
|
|
647
806
|
state.processedMessages.add(messageId);
|
|
648
|
-
const tokens =
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
const
|
|
807
|
+
const tokens = info2["tokens"];
|
|
808
|
+
if (tokens == null || typeof tokens !== "object")
|
|
809
|
+
throw new Error("message.updated: info.tokens is required");
|
|
810
|
+
const tokensObj = tokens;
|
|
811
|
+
const inputTokens = tokensObj["input"];
|
|
812
|
+
const outputTokens = tokensObj["output"];
|
|
813
|
+
const reasoningTokens = tokensObj["reasoning"];
|
|
814
|
+
if (typeof inputTokens !== "number" || typeof outputTokens !== "number")
|
|
815
|
+
throw new Error("message.updated: tokens.input and tokens.output must be numbers");
|
|
816
|
+
const providerID = info2["providerID"];
|
|
817
|
+
const modelID = info2["modelID"];
|
|
818
|
+
if (typeof providerID !== "string" || providerID === "" || typeof modelID !== "string" || modelID === "")
|
|
819
|
+
throw new Error(
|
|
820
|
+
"message.updated: providerID and modelID are required non-empty strings"
|
|
821
|
+
);
|
|
654
822
|
const modelName = `${providerID}/${modelID}`;
|
|
655
|
-
|
|
656
|
-
const outputText =
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
666
|
-
}
|
|
667
|
-
const llmSpan = traceShipper.startSpan({
|
|
668
|
-
name: modelName,
|
|
669
|
-
parent: state.currentTurnSpan.ids,
|
|
670
|
-
eventId: state.currentEventId,
|
|
671
|
-
attributes: llmAttrs
|
|
672
|
-
});
|
|
673
|
-
if (typeof time["created"] === "number") {
|
|
674
|
-
llmSpan.startTimeUnixNano = String(time["created"] * 1e6);
|
|
823
|
+
const finishReason = info2["finish"];
|
|
824
|
+
const outputText = state.outputParts.get(messageId);
|
|
825
|
+
const reasoningText = state.reasoningParts.get(messageId);
|
|
826
|
+
const isToolCallsCompletion = finishReason === "tool-calls";
|
|
827
|
+
const hasIntermediateReasoning = reasoningText !== void 0 && reasoningText.trim().length > 0;
|
|
828
|
+
if (outputText === void 0 && !(isToolCallsCompletion && hasIntermediateReasoning))
|
|
829
|
+
return;
|
|
830
|
+
if (isToolCallsCompletion && !hasIntermediateReasoning) {
|
|
831
|
+
await traceShipper.flush();
|
|
832
|
+
return;
|
|
675
833
|
}
|
|
676
|
-
const msgError =
|
|
677
|
-
const errorForSpan = msgError ?
|
|
834
|
+
const msgError = info2["error"];
|
|
835
|
+
const errorForSpan = msgError ? (() => {
|
|
836
|
+
var _a2, _b2;
|
|
837
|
+
const msg = (_b2 = (_a2 = msgError.data) == null ? void 0 : _a2.message) != null ? _b2 : msgError.name;
|
|
838
|
+
const name = msgError.name;
|
|
839
|
+
if (msg == null && name == null)
|
|
840
|
+
throw new Error("message.updated: error object must have name or data.message");
|
|
841
|
+
return `${msg != null ? msg : name}
|
|
678
842
|
|
|
679
|
-
type: ${
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
843
|
+
type: ${name != null ? name : "UnknownError"}`;
|
|
844
|
+
})() : void 0;
|
|
845
|
+
if (isToolCallsCompletion) {
|
|
846
|
+
if (reasoningText === void 0)
|
|
847
|
+
throw new Error("message.updated: reasoningParts missing for tool-call continuation");
|
|
848
|
+
const llmAttrs = [
|
|
849
|
+
attrString("ai.operationId", "generateText"),
|
|
850
|
+
attrString("ai.response.text", reasoningText),
|
|
851
|
+
attrString("gen_ai.system", providerID),
|
|
852
|
+
attrString("gen_ai.request.model", modelID),
|
|
853
|
+
attrInt("gen_ai.usage.input_tokens", inputTokens),
|
|
854
|
+
attrInt("gen_ai.usage.output_tokens", outputTokens)
|
|
855
|
+
];
|
|
856
|
+
if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
|
|
857
|
+
llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
688
858
|
}
|
|
689
|
-
|
|
859
|
+
const llmParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : state.currentRootSpan.ids;
|
|
860
|
+
const llmSpan = traceShipper.startSpan({
|
|
861
|
+
name: modelName,
|
|
862
|
+
parent: llmParent,
|
|
863
|
+
eventId: state.currentEventId,
|
|
864
|
+
attributes: llmAttrs
|
|
865
|
+
});
|
|
866
|
+
if (typeof timeObj["created"] === "number") {
|
|
867
|
+
llmSpan.startTimeUnixNano = String(Math.floor(timeObj["created"])) + "000000";
|
|
868
|
+
}
|
|
869
|
+
traceShipper.endSpan(llmSpan, { error: errorForSpan });
|
|
870
|
+
await traceShipper.flush();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (outputText === void 0) return;
|
|
874
|
+
const rootSpan = state.currentRootSpan;
|
|
875
|
+
if (state.parentId) {
|
|
876
|
+
const llmAttrs = [
|
|
877
|
+
attrString("ai.operationId", "generateText"),
|
|
878
|
+
attrString("ai.prompt", state.currentInput),
|
|
879
|
+
attrString("ai.response.text", outputText),
|
|
880
|
+
attrString("gen_ai.system", providerID),
|
|
881
|
+
attrString("gen_ai.request.model", modelID),
|
|
882
|
+
attrInt("gen_ai.usage.input_tokens", inputTokens),
|
|
883
|
+
attrInt("gen_ai.usage.output_tokens", outputTokens)
|
|
884
|
+
];
|
|
885
|
+
if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
|
|
886
|
+
llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
887
|
+
}
|
|
888
|
+
if (!state.parentTaskSpanIds) {
|
|
889
|
+
traceShipper.endSpan(rootSpan, {
|
|
890
|
+
error: "Missing strict parent task for child session"
|
|
891
|
+
});
|
|
892
|
+
state.currentRootSpan = void 0;
|
|
893
|
+
state.currentEventId = void 0;
|
|
894
|
+
await traceShipper.flush();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const finalLlmSpan = traceShipper.startSpan({
|
|
898
|
+
name: modelName,
|
|
899
|
+
parent: state.parentTaskSpanIds,
|
|
900
|
+
eventId: state.currentEventId,
|
|
901
|
+
attributes: llmAttrs
|
|
902
|
+
});
|
|
903
|
+
if (typeof timeObj["created"] === "number") {
|
|
904
|
+
finalLlmSpan.startTimeUnixNano = String(Math.floor(timeObj["created"])) + "000000";
|
|
905
|
+
}
|
|
906
|
+
traceShipper.endSpan(finalLlmSpan, { error: errorForSpan });
|
|
907
|
+
traceShipper.endSpan(rootSpan, {
|
|
908
|
+
attributes: [
|
|
909
|
+
attrString("is_subagent", "true"),
|
|
910
|
+
attrString("parent_session_id", state.parentId)
|
|
911
|
+
],
|
|
912
|
+
error: errorForSpan
|
|
913
|
+
});
|
|
914
|
+
} else {
|
|
915
|
+
rootSpan.name = modelName;
|
|
916
|
+
const rootAttrs = [
|
|
917
|
+
attrString("ai.operationId", "generateText"),
|
|
918
|
+
attrString("ai.prompt", state.currentInput),
|
|
919
|
+
attrString("ai.response.text", outputText),
|
|
920
|
+
attrString("gen_ai.system", providerID),
|
|
921
|
+
attrString("gen_ai.request.model", modelID),
|
|
922
|
+
attrInt("gen_ai.usage.input_tokens", inputTokens),
|
|
923
|
+
attrInt("gen_ai.usage.output_tokens", outputTokens)
|
|
924
|
+
];
|
|
925
|
+
if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
|
|
926
|
+
rootAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
927
|
+
}
|
|
928
|
+
traceShipper.endSpan(rootSpan, {
|
|
929
|
+
attributes: rootAttrs,
|
|
930
|
+
error: errorForSpan
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
state.currentRootSpan = void 0;
|
|
934
|
+
const hasAssistantResponse = outputText.trim().length > 0;
|
|
935
|
+
if (!hasAssistantResponse) {
|
|
936
|
+
await traceShipper.flush();
|
|
937
|
+
state.currentEventId = void 0;
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (!state.parentId) {
|
|
941
|
+
await eventShipper.finish(state.currentEventId, {
|
|
942
|
+
userId: state.sessionId,
|
|
943
|
+
model: modelName,
|
|
944
|
+
output: outputText,
|
|
945
|
+
properties: {
|
|
946
|
+
turn_count: state.turnNumber,
|
|
947
|
+
tool_count: state.toolCount
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
690
951
|
await traceShipper.flush();
|
|
691
|
-
|
|
692
|
-
messageId,
|
|
693
|
-
modelName,
|
|
694
|
-
inputTokens,
|
|
695
|
-
outputTokens,
|
|
696
|
-
reasoningTokens
|
|
697
|
-
});
|
|
952
|
+
state.currentEventId = void 0;
|
|
698
953
|
} else if (event.type === "session.idle") {
|
|
699
954
|
if (!sessionID) return;
|
|
700
955
|
const state = sessions.get(String(sessionID));
|
|
701
956
|
if (!state) return;
|
|
702
|
-
if (state.currentTurnSpan) {
|
|
703
|
-
traceShipper.endSpan(state.currentTurnSpan);
|
|
704
|
-
state.currentTurnSpan = void 0;
|
|
705
|
-
}
|
|
706
957
|
if (state.currentRootSpan) {
|
|
707
958
|
traceShipper.endSpan(state.currentRootSpan);
|
|
708
959
|
state.currentRootSpan = void 0;
|
|
709
960
|
}
|
|
710
961
|
if (state.currentEventId) {
|
|
711
|
-
await eventShipper.finish(state.currentEventId, {
|
|
712
|
-
userId: state.userId,
|
|
713
|
-
output: state.currentOutput || void 0,
|
|
714
|
-
properties: {
|
|
715
|
-
turn_count: state.turnNumber,
|
|
716
|
-
tool_count: state.toolCount
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
962
|
state.currentEventId = void 0;
|
|
720
963
|
}
|
|
721
|
-
await traceShipper.flush();
|
|
722
|
-
|
|
964
|
+
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
965
|
+
if (state.parentId) {
|
|
966
|
+
cleanupSessionParentMap(String(sessionID));
|
|
967
|
+
sessions.delete(String(sessionID));
|
|
968
|
+
} else {
|
|
969
|
+
}
|
|
723
970
|
} else if (event.type === "session.deleted") {
|
|
724
971
|
if (!sessionID) return;
|
|
725
972
|
const state = sessions.get(String(sessionID));
|
|
726
973
|
if (!state) return;
|
|
727
|
-
if (state.currentTurnSpan) {
|
|
728
|
-
traceShipper.endSpan(state.currentTurnSpan);
|
|
729
|
-
state.currentTurnSpan = void 0;
|
|
730
|
-
}
|
|
731
974
|
if (state.currentRootSpan) {
|
|
732
975
|
traceShipper.endSpan(state.currentRootSpan);
|
|
733
976
|
state.currentRootSpan = void 0;
|
|
734
977
|
}
|
|
735
978
|
if (state.currentEventId) {
|
|
736
|
-
await eventShipper.finish(state.currentEventId, {
|
|
737
|
-
userId: state.userId,
|
|
738
|
-
output: state.currentOutput || void 0,
|
|
739
|
-
model: state.model,
|
|
740
|
-
properties: {
|
|
741
|
-
turn_count: state.turnNumber,
|
|
742
|
-
tool_count: state.toolCount
|
|
743
|
-
}
|
|
744
|
-
});
|
|
745
979
|
state.currentEventId = void 0;
|
|
746
980
|
}
|
|
981
|
+
cleanupSessionParentMap(String(sessionID));
|
|
747
982
|
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
748
983
|
sessions.delete(String(sessionID));
|
|
749
|
-
log("Session deleted \u2014 flushed", { sessionID });
|
|
750
984
|
} else if (event.type === "session.error") {
|
|
751
|
-
const errorSessionID = props["sessionID"]
|
|
752
|
-
if (
|
|
985
|
+
const errorSessionID = (_c = props["sessionID"]) != null ? _c : sessionID;
|
|
986
|
+
if (errorSessionID === void 0 || errorSessionID === "")
|
|
987
|
+
throw new Error("session.error: missing sessionID");
|
|
753
988
|
const state = sessions.get(String(errorSessionID));
|
|
754
989
|
if (!state) return;
|
|
755
990
|
const errorObj = props["error"];
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
const
|
|
991
|
+
if (errorObj == null) throw new Error("session.error: missing error object");
|
|
992
|
+
const errorName = errorObj.name;
|
|
993
|
+
const errorMessage = (_e = (_d = errorObj.data) == null ? void 0 : _d.message) != null ? _e : errorObj.name;
|
|
994
|
+
if (errorName == null && errorMessage == null)
|
|
995
|
+
throw new Error("session.error: error object must have name or data.message");
|
|
996
|
+
const errorStr = `${errorMessage != null ? errorMessage : errorName}
|
|
759
997
|
|
|
760
|
-
type: ${errorName}`;
|
|
761
|
-
if (state.currentTurnSpan) {
|
|
762
|
-
traceShipper.endSpan(state.currentTurnSpan, { error: errorStr });
|
|
763
|
-
state.currentTurnSpan = void 0;
|
|
764
|
-
}
|
|
998
|
+
type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
765
999
|
if (state.currentRootSpan) {
|
|
766
1000
|
traceShipper.endSpan(state.currentRootSpan, { error: errorStr });
|
|
767
1001
|
state.currentRootSpan = void 0;
|
|
768
1002
|
}
|
|
769
1003
|
if (state.currentEventId) {
|
|
770
|
-
await eventShipper.finish(state.currentEventId, {
|
|
771
|
-
userId: state.userId,
|
|
772
|
-
properties: {
|
|
773
|
-
error: errorStr,
|
|
774
|
-
turn_count: state.turnNumber,
|
|
775
|
-
tool_count: state.toolCount
|
|
776
|
-
}
|
|
777
|
-
});
|
|
778
1004
|
state.currentEventId = void 0;
|
|
779
1005
|
}
|
|
1006
|
+
cleanupSessionParentMap(String(errorSessionID));
|
|
780
1007
|
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
781
1008
|
sessions.delete(String(errorSessionID));
|
|
782
|
-
log("Session error handled", { errorSessionID, errorName });
|
|
783
|
-
} else {
|
|
784
|
-
log(`unhandled event: ${event.type}`);
|
|
785
1009
|
}
|
|
786
1010
|
} catch (err) {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
level: "error",
|
|
791
|
-
message: `Error in event hook: ${err instanceof Error ? err.message : String(err)}`
|
|
792
|
-
}
|
|
793
|
-
})) == null ? void 0 : _j.catch) == null ? void 0 : _k.call(_j, () => {
|
|
794
|
-
});
|
|
1011
|
+
console.log(
|
|
1012
|
+
`[raindrop-ai/opencode-plugin] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1013
|
+
);
|
|
795
1014
|
}
|
|
796
1015
|
},
|
|
797
1016
|
// ------------------------------------------------------------------
|
|
798
1017
|
// chat.message — user sent a message; start a new turn with a fresh event ID
|
|
799
1018
|
// ------------------------------------------------------------------
|
|
800
1019
|
"chat.message": async (messageInput, output) => {
|
|
801
|
-
var _a, _b;
|
|
802
1020
|
try {
|
|
803
1021
|
const { sessionID } = messageInput;
|
|
804
1022
|
let state = sessions.get(sessionID);
|
|
805
1023
|
if (!state) {
|
|
806
|
-
state =
|
|
807
|
-
sessionId: sessionID,
|
|
808
|
-
userId: (_a = config.userId) != null ? _a : sessionID,
|
|
809
|
-
turnNumber: 0,
|
|
810
|
-
toolCount: 0,
|
|
811
|
-
startTime: Date.now(),
|
|
812
|
-
currentInput: "",
|
|
813
|
-
currentOutput: "",
|
|
814
|
-
outputParts: /* @__PURE__ */ new Map(),
|
|
815
|
-
reasoningParts: /* @__PURE__ */ new Map(),
|
|
816
|
-
toolCalls: /* @__PURE__ */ new Map(),
|
|
817
|
-
toolStartTimes: /* @__PURE__ */ new Map(),
|
|
818
|
-
toolSpanStarts: /* @__PURE__ */ new Map(),
|
|
819
|
-
toolCallMessageIds: /* @__PURE__ */ new Map(),
|
|
820
|
-
processedMessages: /* @__PURE__ */ new Set(),
|
|
821
|
-
attachments: []
|
|
822
|
-
};
|
|
1024
|
+
state = createSessionState(sessionID);
|
|
823
1025
|
sessions.set(sessionID, state);
|
|
824
1026
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
state.
|
|
828
|
-
|
|
829
|
-
if (!state.currentRootSpan) {
|
|
830
|
-
state.currentRootSpan = traceShipper.startSpan({
|
|
831
|
-
name: `OpenCode: ${getProjectName(worktree)}`,
|
|
832
|
-
eventId: sessionID,
|
|
833
|
-
attributes: [
|
|
834
|
-
attrString("workspace", worktree),
|
|
835
|
-
attrString("directory", directory),
|
|
836
|
-
attrString("hostname", hostname),
|
|
837
|
-
attrString("os", os)
|
|
838
|
-
]
|
|
839
|
-
});
|
|
1027
|
+
applyChildSessionParentToState(sessionID);
|
|
1028
|
+
if (state.currentRootSpan) {
|
|
1029
|
+
traceShipper.endSpan(state.currentRootSpan);
|
|
1030
|
+
state.currentRootSpan = void 0;
|
|
840
1031
|
}
|
|
841
1032
|
state.turnNumber++;
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1033
|
+
if (output == null) throw new Error("chat.message: output is required");
|
|
1034
|
+
const parts = output.parts;
|
|
1035
|
+
if (parts == null) throw new Error("chat.message: output.parts is required");
|
|
845
1036
|
const textParts = [];
|
|
846
1037
|
const newAttachments = [];
|
|
847
1038
|
for (const part of parts) {
|
|
@@ -852,26 +1043,52 @@ type: ${errorName}`;
|
|
|
852
1043
|
const filename = part["filename"];
|
|
853
1044
|
const url = part["url"];
|
|
854
1045
|
if (url && (mimeType == null ? void 0 : mimeType.startsWith("image/"))) {
|
|
1046
|
+
if (filename === void 0 || filename === "")
|
|
1047
|
+
throw new Error("chat.message: file part must have filename");
|
|
855
1048
|
newAttachments.push({ type: "image", role: "input", name: filename, value: url });
|
|
856
1049
|
}
|
|
857
1050
|
}
|
|
858
1051
|
}
|
|
859
1052
|
const userText = textParts.join("\n");
|
|
860
1053
|
state.currentInput = userText;
|
|
861
|
-
if (
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1054
|
+
if (state.parentId) {
|
|
1055
|
+
if (!state.parentTaskSpanIds || !state.parentEventContext) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
state.currentEventId = state.parentEventContext.eventId;
|
|
1059
|
+
state.currentRootSpan = traceShipper.startSpan({
|
|
1060
|
+
name: "Subagent",
|
|
1061
|
+
parent: state.parentTaskSpanIds,
|
|
1062
|
+
eventId: state.currentEventId,
|
|
1063
|
+
attributes: [
|
|
1064
|
+
attrString("workspace", worktree),
|
|
1065
|
+
attrString("directory", directory),
|
|
1066
|
+
attrString("hostname", hostname),
|
|
1067
|
+
attrString("os", os),
|
|
1068
|
+
attrString("is_subagent", "true"),
|
|
1069
|
+
attrString("parent_session_id", state.parentId)
|
|
1070
|
+
]
|
|
1071
|
+
});
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
state.currentEventId = generateId();
|
|
1075
|
+
state.currentRootSpan = traceShipper.startSpan({
|
|
1076
|
+
name: "ai.event",
|
|
865
1077
|
eventId: state.currentEventId,
|
|
866
|
-
attributes: [
|
|
1078
|
+
attributes: [
|
|
1079
|
+
attrString("workspace", worktree),
|
|
1080
|
+
attrString("directory", directory),
|
|
1081
|
+
attrString("hostname", hostname),
|
|
1082
|
+
attrString("os", os)
|
|
1083
|
+
]
|
|
867
1084
|
});
|
|
868
1085
|
await eventShipper.patch(state.currentEventId, {
|
|
869
1086
|
isPending: true,
|
|
870
|
-
userId: state.
|
|
1087
|
+
userId: state.sessionId,
|
|
871
1088
|
convoId: state.sessionId,
|
|
872
1089
|
eventName: config.eventName,
|
|
873
|
-
input: userText
|
|
874
|
-
|
|
1090
|
+
input: userText,
|
|
1091
|
+
...newAttachments.length > 0 ? { attachments: newAttachments } : {},
|
|
875
1092
|
properties: {
|
|
876
1093
|
workspace: worktree,
|
|
877
1094
|
directory,
|
|
@@ -881,74 +1098,143 @@ type: ${errorName}`;
|
|
|
881
1098
|
tool_count: state.toolCount
|
|
882
1099
|
}
|
|
883
1100
|
});
|
|
884
|
-
log("Chat message \u2014 new turn started", {
|
|
885
|
-
sessionID,
|
|
886
|
-
eventId: state.currentEventId,
|
|
887
|
-
turnNumber: state.turnNumber,
|
|
888
|
-
inputLength: userText.length,
|
|
889
|
-
attachments: newAttachments.length
|
|
890
|
-
});
|
|
891
1101
|
} catch (err) {
|
|
892
|
-
log("Error in chat.message hook", { error: String(err) });
|
|
893
1102
|
}
|
|
894
1103
|
},
|
|
895
1104
|
// ------------------------------------------------------------------
|
|
896
1105
|
// tool.execute.before — record start time only (span created atomically in after)
|
|
1106
|
+
// For child sessions, tool spans are parented directly to the parent task span.
|
|
897
1107
|
// ------------------------------------------------------------------
|
|
898
1108
|
"tool.execute.before": async (toolInput, _output) => {
|
|
1109
|
+
var _a;
|
|
899
1110
|
try {
|
|
900
1111
|
const { tool, sessionID, callID } = toolInput;
|
|
901
1112
|
const state = sessions.get(sessionID);
|
|
902
|
-
if (!state || !state.
|
|
903
|
-
state.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1113
|
+
if (!state || !state.currentEventId) return;
|
|
1114
|
+
const spanParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : (_a = state.currentRootSpan) == null ? void 0 : _a.ids;
|
|
1115
|
+
if (!spanParent) return;
|
|
1116
|
+
const startTimeUnixNano = nowUnixNanoString();
|
|
1117
|
+
if (tool === "task") {
|
|
1118
|
+
const liveTaskSpan = traceShipper.startSpan({
|
|
1119
|
+
name: "ai.toolCall",
|
|
1120
|
+
parent: spanParent,
|
|
1121
|
+
eventId: state.currentEventId,
|
|
1122
|
+
attributes: [
|
|
1123
|
+
attrString("ai.operationId", "ai.toolCall"),
|
|
1124
|
+
attrString("ai.toolCall.name", tool),
|
|
1125
|
+
attrString("ai.toolCall.id", callID)
|
|
1126
|
+
]
|
|
1127
|
+
});
|
|
1128
|
+
liveTaskSpan.startTimeUnixNano = startTimeUnixNano;
|
|
1129
|
+
const ctx = {
|
|
1130
|
+
ids: liveTaskSpan.ids,
|
|
1131
|
+
eventContext: {
|
|
1132
|
+
eventId: state.currentEventId,
|
|
1133
|
+
userId: state.sessionId,
|
|
1134
|
+
convoId: state.sessionId
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
taskContexts.set(callKey(sessionID, callID), ctx);
|
|
1138
|
+
let running = runningTaskCallsBySession.get(sessionID);
|
|
1139
|
+
if (running == null) {
|
|
1140
|
+
running = /* @__PURE__ */ new Set();
|
|
1141
|
+
runningTaskCallsBySession.set(sessionID, running);
|
|
1142
|
+
}
|
|
1143
|
+
running.add(callID);
|
|
1144
|
+
resolvePendingChildrenForCall(sessionID, callID, "tool.execute.before");
|
|
1145
|
+
state.toolSpanStarts.set(callID, {
|
|
1146
|
+
startTimeUnixNano,
|
|
1147
|
+
parent: spanParent,
|
|
1148
|
+
eventId: state.currentEventId,
|
|
1149
|
+
name: tool,
|
|
1150
|
+
liveSpan: liveTaskSpan
|
|
1151
|
+
});
|
|
1152
|
+
} else {
|
|
1153
|
+
state.toolSpanStarts.set(callID, {
|
|
1154
|
+
startTimeUnixNano,
|
|
1155
|
+
parent: spanParent,
|
|
1156
|
+
eventId: state.currentEventId,
|
|
1157
|
+
name: tool
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
911
1160
|
} catch (err) {
|
|
912
|
-
log(
|
|
1161
|
+
console.log(
|
|
1162
|
+
`[raindrop-ai/opencode-plugin] [error] Error in tool.execute.before hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1163
|
+
);
|
|
913
1164
|
}
|
|
914
1165
|
},
|
|
915
1166
|
// ------------------------------------------------------------------
|
|
916
1167
|
// tool.execute.after — create tool span atomically with both start and end times
|
|
917
1168
|
// ------------------------------------------------------------------
|
|
918
1169
|
"tool.execute.after": async (toolInput, result) => {
|
|
919
|
-
var _a;
|
|
920
1170
|
try {
|
|
921
|
-
const { tool, sessionID, callID } = toolInput;
|
|
1171
|
+
const { tool, sessionID, callID, args } = toolInput;
|
|
922
1172
|
const state = sessions.get(sessionID);
|
|
923
1173
|
if (!state || !state.currentEventId) return;
|
|
924
1174
|
state.toolCount++;
|
|
925
|
-
|
|
926
|
-
|
|
1175
|
+
if (args === void 0 || args === null)
|
|
1176
|
+
throw new Error("tool.execute.after: args is required");
|
|
1177
|
+
const toolCallArgs = typeof args === "string" ? args : JSON.stringify(args);
|
|
927
1178
|
const startInfo = state.toolSpanStarts.get(callID);
|
|
928
1179
|
state.toolSpanStarts.delete(callID);
|
|
1180
|
+
const resultMetadata = result.metadata;
|
|
1181
|
+
const childSessionId = resultMetadata == null ? void 0 : resultMetadata["sessionId"];
|
|
1182
|
+
if (tool === "task" && childSessionId) {
|
|
1183
|
+
mapChildSessionToParent.set(childSessionId, { parentId: sessionID });
|
|
1184
|
+
attachChildSessionToParentTask(childSessionId, sessionID, callID, "tool.execute.after");
|
|
1185
|
+
}
|
|
929
1186
|
if (startInfo) {
|
|
930
1187
|
const endTimeUnixNano = nowUnixNanoString();
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1188
|
+
if (!("output" in result) || result.output === void 0)
|
|
1189
|
+
throw new Error("tool.execute.after: result.output is required");
|
|
1190
|
+
let toolResult;
|
|
1191
|
+
try {
|
|
1192
|
+
toolResult = JSON.stringify(result.output);
|
|
1193
|
+
} catch (err) {
|
|
1194
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1195
|
+
throw new Error(`tool.execute.after: result.output must be JSON-serializable: ${msg}`);
|
|
1196
|
+
}
|
|
1197
|
+
if (startInfo.liveSpan) {
|
|
1198
|
+
traceShipper.endSpan(startInfo.liveSpan, {
|
|
1199
|
+
attributes: [
|
|
1200
|
+
attrString("ai.toolCall.args", toolCallArgs),
|
|
1201
|
+
attrString("ai.toolCall.result", toolResult)
|
|
1202
|
+
]
|
|
1203
|
+
});
|
|
1204
|
+
} else {
|
|
1205
|
+
traceShipper.createSpan({
|
|
1206
|
+
name: "ai.toolCall",
|
|
1207
|
+
parent: startInfo.parent,
|
|
1208
|
+
eventId: startInfo.eventId,
|
|
1209
|
+
startTimeUnixNano: startInfo.startTimeUnixNano,
|
|
1210
|
+
endTimeUnixNano,
|
|
1211
|
+
attributes: [
|
|
1212
|
+
attrString("ai.operationId", "ai.toolCall"),
|
|
1213
|
+
attrString("ai.toolCall.name", tool),
|
|
1214
|
+
attrString("ai.toolCall.id", callID),
|
|
1215
|
+
attrString("ai.toolCall.args", toolCallArgs),
|
|
1216
|
+
attrString("ai.toolCall.result", toolResult)
|
|
1217
|
+
]
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (tool === "task") {
|
|
1222
|
+
const running = runningTaskCallsBySession.get(sessionID);
|
|
1223
|
+
if (running) {
|
|
1224
|
+
running.delete(callID);
|
|
1225
|
+
if (running.size === 0) runningTaskCallsBySession.delete(sessionID);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (!state.parentId) {
|
|
1229
|
+
await eventShipper.patch(state.currentEventId, {
|
|
1230
|
+
userId: state.sessionId,
|
|
1231
|
+
properties: { tool_count: state.toolCount }
|
|
943
1232
|
});
|
|
944
1233
|
}
|
|
945
|
-
await eventShipper.patch(state.currentEventId, {
|
|
946
|
-
userId: state.userId,
|
|
947
|
-
properties: { tool_count: state.toolCount }
|
|
948
|
-
});
|
|
949
|
-
log("Tool execute after", { tool, sessionID, callID });
|
|
950
1234
|
} catch (err) {
|
|
951
|
-
log(
|
|
1235
|
+
console.log(
|
|
1236
|
+
`[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1237
|
+
);
|
|
952
1238
|
}
|
|
953
1239
|
}
|
|
954
1240
|
};
|
|
@@ -957,18 +1243,9 @@ type: ${errorName}`;
|
|
|
957
1243
|
// src/index.ts
|
|
958
1244
|
async function plugin(input) {
|
|
959
1245
|
var _a;
|
|
960
|
-
const { client } = input;
|
|
961
1246
|
const config = loadConfig(input.directory);
|
|
962
1247
|
function appLog(level, message) {
|
|
963
|
-
|
|
964
|
-
(_b = (_a2 = client.app.log({
|
|
965
|
-
body: {
|
|
966
|
-
service: "raindrop-opencode",
|
|
967
|
-
level,
|
|
968
|
-
message
|
|
969
|
-
}
|
|
970
|
-
})) == null ? void 0 : _a2.catch) == null ? void 0 : _b.call(_a2, () => {
|
|
971
|
-
});
|
|
1248
|
+
console.log(`[raindrop-ai/opencode-plugin] [${level}] ${message}`);
|
|
972
1249
|
}
|
|
973
1250
|
if (!config.writeKey) {
|
|
974
1251
|
appLog(
|
|
@@ -992,7 +1269,6 @@ async function plugin(input) {
|
|
|
992
1269
|
});
|
|
993
1270
|
const worktree = (_a = input.worktree) != null ? _a : input.directory;
|
|
994
1271
|
const hooks = createHooks(
|
|
995
|
-
client,
|
|
996
1272
|
config,
|
|
997
1273
|
worktree,
|
|
998
1274
|
input.directory,
|