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