@raindrop-ai/opencode-plugin 0.0.1 → 0.0.3
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 +582 -281
- package/dist/index.d.cts +2 -34
- package/dist/index.d.ts +2 -34
- package/dist/index.js +582 -281
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -47,7 +47,7 @@ function loadConfig(projectDirectory) {
|
|
|
47
47
|
}
|
|
48
48
|
return {
|
|
49
49
|
writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
|
|
50
|
-
endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai",
|
|
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
52
|
userId: merged.user_id,
|
|
53
53
|
debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false
|
|
@@ -96,10 +96,11 @@ function base64Encode(bytes) {
|
|
|
96
96
|
var SERVICE_NAME = "raindrop.opencode-plugin";
|
|
97
97
|
var PLUGIN_VERSION = "0.1.0";
|
|
98
98
|
var SpanStatusCode = {
|
|
99
|
-
UNSET: 0,
|
|
100
|
-
OK: 1,
|
|
101
99
|
ERROR: 2
|
|
102
100
|
};
|
|
101
|
+
function generateId() {
|
|
102
|
+
return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
|
|
103
|
+
}
|
|
103
104
|
function createSpanIds(parent) {
|
|
104
105
|
const traceId = parent ? parent.traceIdB64 : base64Encode(randomBytes(16));
|
|
105
106
|
const spanId = base64Encode(randomBytes(8));
|
|
@@ -122,7 +123,11 @@ function attrInt(key, value) {
|
|
|
122
123
|
return { key, value: { intValue: String(Math.trunc(value)) } };
|
|
123
124
|
}
|
|
124
125
|
function buildOtlpSpan(args) {
|
|
125
|
-
const attrs =
|
|
126
|
+
const attrs = [
|
|
127
|
+
attrString("span.id", args.ids.spanIdB64),
|
|
128
|
+
attrString("span.parent.id", args.ids.parentSpanIdB64),
|
|
129
|
+
...args.attributes
|
|
130
|
+
].filter((x) => x !== void 0);
|
|
126
131
|
const span = {
|
|
127
132
|
traceId: args.ids.traceIdB64,
|
|
128
133
|
spanId: args.ids.spanIdB64,
|
|
@@ -192,9 +197,7 @@ async function withRetry(operation, opName, opts) {
|
|
|
192
197
|
}
|
|
193
198
|
if (delay > 0) await wait(delay);
|
|
194
199
|
} else if (opts.debug) {
|
|
195
|
-
console.log(
|
|
196
|
-
`[raindrop-ai/opencode-plugin] ${opName} attempt ${attempt}/${opts.maxAttempts}`
|
|
197
|
-
);
|
|
200
|
+
console.log(`[raindrop-ai/opencode-plugin] ${opName} attempt ${attempt}/${opts.maxAttempts}`);
|
|
198
201
|
}
|
|
199
202
|
try {
|
|
200
203
|
return await operation();
|
|
@@ -428,6 +431,27 @@ var TraceShipper = class {
|
|
|
428
431
|
});
|
|
429
432
|
this.enqueue(otlp);
|
|
430
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Create and enqueue a span with both start and end times (atomic insert).
|
|
436
|
+
* Used for tool spans and any span whose full duration is known at write time.
|
|
437
|
+
*/
|
|
438
|
+
createSpan(args) {
|
|
439
|
+
var _a;
|
|
440
|
+
const ids = createSpanIds(args.parent);
|
|
441
|
+
const attrs = [
|
|
442
|
+
attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)
|
|
443
|
+
];
|
|
444
|
+
if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
|
|
445
|
+
const otlp = buildOtlpSpan({
|
|
446
|
+
ids,
|
|
447
|
+
name: args.name,
|
|
448
|
+
startTimeUnixNano: args.startTimeUnixNano,
|
|
449
|
+
endTimeUnixNano: args.endTimeUnixNano,
|
|
450
|
+
attributes: attrs,
|
|
451
|
+
status: args.status
|
|
452
|
+
});
|
|
453
|
+
this.enqueue(otlp);
|
|
454
|
+
}
|
|
431
455
|
enqueue(span) {
|
|
432
456
|
if (this.queue.length >= this.maxQueueSize) this.queue.shift();
|
|
433
457
|
this.queue.push(span);
|
|
@@ -453,10 +477,12 @@ var TraceShipper = class {
|
|
|
453
477
|
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
454
478
|
const body = buildExportTraceServiceRequest(batch);
|
|
455
479
|
const url = `${this.baseUrl}traces`;
|
|
480
|
+
const spanNames = batch.map((span) => span.name);
|
|
456
481
|
if (this.debug) {
|
|
457
482
|
console.log("[raindrop-ai/opencode-plugin] sending traces batch", {
|
|
458
483
|
spans: batch.length,
|
|
459
|
-
endpoint: url
|
|
484
|
+
endpoint: url,
|
|
485
|
+
spanNames
|
|
460
486
|
});
|
|
461
487
|
}
|
|
462
488
|
const p = postJson(
|
|
@@ -470,14 +496,14 @@ var TraceShipper = class {
|
|
|
470
496
|
try {
|
|
471
497
|
await p;
|
|
472
498
|
if (this.debug) {
|
|
473
|
-
console.log(`[raindrop-ai/opencode-plugin] sent ${batch.length} spans
|
|
499
|
+
console.log(`[raindrop-ai/opencode-plugin] sent ${batch.length} spans`, {
|
|
500
|
+
spanNames
|
|
501
|
+
});
|
|
474
502
|
}
|
|
475
503
|
} catch (err) {
|
|
476
504
|
if (this.debug) {
|
|
477
505
|
const msg = err instanceof Error ? err.message : String(err);
|
|
478
|
-
console.warn(
|
|
479
|
-
`[raindrop-ai/opencode-plugin] failed to send spans (dropping): ${msg}`
|
|
480
|
-
);
|
|
506
|
+
console.warn(`[raindrop-ai/opencode-plugin] failed to send spans (dropping): ${msg}`);
|
|
481
507
|
}
|
|
482
508
|
}
|
|
483
509
|
} finally {
|
|
@@ -496,281 +522,458 @@ var TraceShipper = class {
|
|
|
496
522
|
}
|
|
497
523
|
};
|
|
498
524
|
|
|
525
|
+
// src/tracing-linkage-helpers.ts
|
|
526
|
+
function createSessionParentMapHelpers({
|
|
527
|
+
sessions: sessions2,
|
|
528
|
+
taskContexts: taskContexts2,
|
|
529
|
+
runningTaskCallsBySession: runningTaskCallsBySession2,
|
|
530
|
+
mapChildSessionToParent: mapChildSessionToParent2,
|
|
531
|
+
pendingChildSessionsByCallKey: pendingChildSessionsByCallKey2,
|
|
532
|
+
callKey: callKey2,
|
|
533
|
+
log
|
|
534
|
+
}) {
|
|
535
|
+
function applyChildSessionParentToState(childSessionId) {
|
|
536
|
+
const parentInfo = mapChildSessionToParent2.get(childSessionId);
|
|
537
|
+
const state = sessions2.get(childSessionId);
|
|
538
|
+
if (!parentInfo || !state) return;
|
|
539
|
+
state.parentId = parentInfo.parentId;
|
|
540
|
+
state.parentTaskSpanIds = parentInfo.parentTaskSpanIds;
|
|
541
|
+
state.parentEventContext = parentInfo.eventContext;
|
|
542
|
+
}
|
|
543
|
+
function queuePendingChildByCallKey(key, childSessionId) {
|
|
544
|
+
var _a;
|
|
545
|
+
const pending = (_a = pendingChildSessionsByCallKey2.get(key)) != null ? _a : /* @__PURE__ */ new Set();
|
|
546
|
+
pending.add(childSessionId);
|
|
547
|
+
pendingChildSessionsByCallKey2.set(key, pending);
|
|
548
|
+
}
|
|
549
|
+
function attachChildSessionToParentTask(childSessionId, parentId, parentCallId, source) {
|
|
550
|
+
const key = callKey2(parentId, parentCallId);
|
|
551
|
+
const ctx = taskContexts2.get(key);
|
|
552
|
+
if (!ctx) {
|
|
553
|
+
queuePendingChildByCallKey(key, childSessionId);
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
const parentInfo = {
|
|
557
|
+
parentId,
|
|
558
|
+
parentTaskSpanIds: ctx.ids,
|
|
559
|
+
eventContext: ctx.eventContext
|
|
560
|
+
};
|
|
561
|
+
mapChildSessionToParent2.set(childSessionId, parentInfo);
|
|
562
|
+
applyChildSessionParentToState(childSessionId);
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
function resolvePendingChildrenForCall(parentId, parentCallId, source) {
|
|
566
|
+
const key = callKey2(parentId, parentCallId);
|
|
567
|
+
const pending = pendingChildSessionsByCallKey2.get(key);
|
|
568
|
+
if (pending && pending.size > 0) {
|
|
569
|
+
for (const childSessionId of [...pending]) {
|
|
570
|
+
if (attachChildSessionToParentTask(childSessionId, parentId, parentCallId, source)) {
|
|
571
|
+
pending.delete(childSessionId);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (pending.size === 0) pendingChildSessionsByCallKey2.delete(key);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function cleanupSessionParentMap(sessionId) {
|
|
578
|
+
mapChildSessionToParent2.delete(sessionId);
|
|
579
|
+
runningTaskCallsBySession2.delete(sessionId);
|
|
580
|
+
for (const [key, pending] of pendingChildSessionsByCallKey2.entries()) {
|
|
581
|
+
pending.delete(sessionId);
|
|
582
|
+
if (pending.size === 0) pendingChildSessionsByCallKey2.delete(key);
|
|
583
|
+
}
|
|
584
|
+
for (const key of [...taskContexts2.keys()]) {
|
|
585
|
+
if (key.startsWith(`${sessionId}:`)) taskContexts2.delete(key);
|
|
586
|
+
}
|
|
587
|
+
for (const [childSessionId, parentInfo] of [...mapChildSessionToParent2.entries()]) {
|
|
588
|
+
if (parentInfo.parentId === sessionId) {
|
|
589
|
+
mapChildSessionToParent2.delete(childSessionId);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
applyChildSessionParentToState,
|
|
595
|
+
attachChildSessionToParentTask,
|
|
596
|
+
cleanupSessionParentMap,
|
|
597
|
+
resolvePendingChildrenForCall
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
499
601
|
// src/tracing.ts
|
|
500
602
|
var sessions = /* @__PURE__ */ new Map();
|
|
603
|
+
var taskContexts = /* @__PURE__ */ new Map();
|
|
604
|
+
var runningTaskCallsBySession = /* @__PURE__ */ new Map();
|
|
605
|
+
var mapChildSessionToParent = /* @__PURE__ */ new Map();
|
|
606
|
+
var pendingChildSessionsByCallKey = /* @__PURE__ */ new Map();
|
|
607
|
+
function createSessionState(sessionId) {
|
|
608
|
+
return {
|
|
609
|
+
sessionId,
|
|
610
|
+
turnNumber: 0,
|
|
611
|
+
toolCount: 0,
|
|
612
|
+
currentInput: "",
|
|
613
|
+
outputParts: /* @__PURE__ */ new Map(),
|
|
614
|
+
reasoningParts: /* @__PURE__ */ new Map(),
|
|
615
|
+
toolSpanStarts: /* @__PURE__ */ new Map(),
|
|
616
|
+
processedMessages: /* @__PURE__ */ new Set()
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function callKey(sessionId, callId) {
|
|
620
|
+
return `${sessionId}:${callId}`;
|
|
621
|
+
}
|
|
501
622
|
function getHostname() {
|
|
502
|
-
var _a
|
|
623
|
+
var _a;
|
|
503
624
|
try {
|
|
504
625
|
const bunGlobal = globalThis;
|
|
505
626
|
if ((_a = bunGlobal.Bun) == null ? void 0 : _a.hostname) return bunGlobal.Bun.hostname;
|
|
506
627
|
return require("os").hostname();
|
|
507
628
|
} catch (e) {
|
|
508
|
-
|
|
629
|
+
const h = process.env["HOSTNAME"];
|
|
630
|
+
if (h === void 0 || h === "") throw new Error("HOSTNAME env is not set");
|
|
631
|
+
return h;
|
|
509
632
|
}
|
|
510
633
|
}
|
|
511
|
-
function
|
|
512
|
-
var _a;
|
|
513
|
-
const parts = worktree.split("/").filter(Boolean);
|
|
514
|
-
return (_a = parts[parts.length - 1]) != null ? _a : "unknown";
|
|
515
|
-
}
|
|
516
|
-
function createHooks(client, config, worktree, directory, eventShipper, traceShipper) {
|
|
634
|
+
function createHooks(config, worktree, directory, eventShipper, traceShipper) {
|
|
517
635
|
function log(msg, data) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
})) == null ? void 0 : _a.catch) == null ? void 0 : _b.call(_a, () => {
|
|
527
|
-
});
|
|
636
|
+
const level = config.debug ? "info" : "debug";
|
|
637
|
+
const prefix = `[raindrop-ai/opencode-plugin] [${level}] ${msg}`;
|
|
638
|
+
if (data !== void 0) {
|
|
639
|
+
console.log(prefix, data);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
console.log(prefix);
|
|
528
643
|
}
|
|
529
|
-
|
|
644
|
+
const hostname = getHostname();
|
|
645
|
+
const os = process.platform;
|
|
646
|
+
const {
|
|
647
|
+
applyChildSessionParentToState,
|
|
648
|
+
attachChildSessionToParentTask,
|
|
649
|
+
cleanupSessionParentMap,
|
|
650
|
+
resolvePendingChildrenForCall
|
|
651
|
+
} = createSessionParentMapHelpers({
|
|
652
|
+
sessions,
|
|
653
|
+
taskContexts,
|
|
654
|
+
runningTaskCallsBySession,
|
|
655
|
+
mapChildSessionToParent,
|
|
656
|
+
pendingChildSessionsByCallKey,
|
|
657
|
+
callKey,
|
|
658
|
+
log
|
|
659
|
+
});
|
|
530
660
|
return {
|
|
531
661
|
// ------------------------------------------------------------------
|
|
532
662
|
// event — session lifecycle + streaming parts
|
|
533
663
|
// ------------------------------------------------------------------
|
|
534
|
-
event: async ({
|
|
535
|
-
|
|
536
|
-
}) => {
|
|
537
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
664
|
+
event: async ({ event }) => {
|
|
665
|
+
var _a, _b, _c, _d, _e;
|
|
538
666
|
try {
|
|
539
667
|
const props = event.properties;
|
|
540
668
|
const info = props["info"];
|
|
541
|
-
const sessionID = props["sessionID"]
|
|
669
|
+
const sessionID = (_b = (_a = props["sessionID"]) != null ? _a : info == null ? void 0 : info["id"]) != null ? _b : props["id"];
|
|
670
|
+
const sessionEventTypes = ["session.created", "session.idle", "session.deleted", "session.error"];
|
|
671
|
+
if (sessionEventTypes.includes(event.type) && (sessionID === void 0 || sessionID === "")) {
|
|
672
|
+
throw new Error(`event ${event.type}: missing sessionID in properties`);
|
|
673
|
+
}
|
|
542
674
|
if (event.type === "session.created") {
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
675
|
+
if (info == null || typeof info !== "object")
|
|
676
|
+
throw new Error("session.created: props.info is required");
|
|
677
|
+
const parentID = info["parentID"];
|
|
678
|
+
let state = sessions.get(sessionID);
|
|
679
|
+
if (!state) {
|
|
680
|
+
state = createSessionState(sessionID);
|
|
681
|
+
sessions.set(sessionID, state);
|
|
546
682
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const os = process.platform;
|
|
550
|
-
const rootSpan = traceShipper.startSpan({
|
|
551
|
-
name: `OpenCode: ${getProjectName(worktree)}`,
|
|
552
|
-
eventId: sessionID,
|
|
553
|
-
attributes: [
|
|
554
|
-
attrString("workspace", worktree),
|
|
555
|
-
attrString("directory", directory),
|
|
556
|
-
attrString("hostname", hostname),
|
|
557
|
-
attrString("os", os)
|
|
558
|
-
]
|
|
559
|
-
});
|
|
560
|
-
const state = {
|
|
561
|
-
eventId: sessionID,
|
|
562
|
-
rootSpan,
|
|
563
|
-
turnNumber: 0,
|
|
564
|
-
toolCount: 0,
|
|
565
|
-
startTime: Date.now(),
|
|
566
|
-
currentInput: "",
|
|
567
|
-
currentOutput: "",
|
|
568
|
-
outputParts: /* @__PURE__ */ new Map(),
|
|
569
|
-
reasoningParts: /* @__PURE__ */ new Map(),
|
|
570
|
-
toolCalls: /* @__PURE__ */ new Map(),
|
|
571
|
-
toolStartTimes: /* @__PURE__ */ new Map(),
|
|
572
|
-
toolSpans: /* @__PURE__ */ new Map(),
|
|
573
|
-
toolCallMessageIds: /* @__PURE__ */ new Map(),
|
|
574
|
-
processedMessages: /* @__PURE__ */ new Set(),
|
|
575
|
-
attachments: []
|
|
576
|
-
};
|
|
577
|
-
sessions.set(sessionID, state);
|
|
578
|
-
await eventShipper.patch(sessionID, {
|
|
579
|
-
isPending: true,
|
|
580
|
-
userId,
|
|
581
|
-
convoId: sessionID,
|
|
582
|
-
eventName: config.eventName,
|
|
583
|
-
properties: {
|
|
584
|
-
workspace: worktree,
|
|
585
|
-
directory,
|
|
586
|
-
hostname,
|
|
587
|
-
os,
|
|
588
|
-
turn_count: 0,
|
|
589
|
-
tool_count: 0
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
log("Session created", { sessionID, userId });
|
|
683
|
+
state.parentId = parentID;
|
|
684
|
+
if (state.parentId) mapChildSessionToParent.set(sessionID, { parentId: state.parentId });
|
|
593
685
|
} else if (event.type === "message.part.updated") {
|
|
594
686
|
const part = props["part"];
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
687
|
+
if (part == null || typeof part !== "object")
|
|
688
|
+
throw new Error("message.part.updated: props.part is required");
|
|
689
|
+
const partObj = part;
|
|
690
|
+
const partSessionID = partObj["sessionID"];
|
|
691
|
+
const messageId = partObj["messageID"];
|
|
692
|
+
if (partSessionID == null || partSessionID === "")
|
|
693
|
+
throw new Error("message.part.updated: part.sessionID is required");
|
|
694
|
+
if (messageId == null || messageId === "")
|
|
695
|
+
throw new Error("message.part.updated: part.messageID is required");
|
|
599
696
|
const state = sessions.get(partSessionID);
|
|
600
697
|
if (!state) return;
|
|
601
|
-
if (
|
|
602
|
-
state.outputParts.set(messageId,
|
|
603
|
-
} else if (
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if (
|
|
611
|
-
|
|
612
|
-
|
|
698
|
+
if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
|
|
699
|
+
state.outputParts.set(messageId, partObj["text"]);
|
|
700
|
+
} else if (partObj["type"] === "tool" && messageId) {
|
|
701
|
+
const tool = partObj["tool"];
|
|
702
|
+
if (tool === "task") {
|
|
703
|
+
const callID = partObj["callID"];
|
|
704
|
+
const partState = partObj["state"];
|
|
705
|
+
if (callID == null || callID === "")
|
|
706
|
+
throw new Error("message.part.updated: part.callID is required for tool part");
|
|
707
|
+
if (partState == null || typeof partState !== "object")
|
|
708
|
+
throw new Error("message.part.updated: part.state is required for tool part");
|
|
709
|
+
const metadata = partState["metadata"];
|
|
710
|
+
const childSessionId = metadata != null && typeof metadata === "object" ? metadata["sessionId"] : void 0;
|
|
711
|
+
if (childSessionId != null && childSessionId !== "") {
|
|
712
|
+
mapChildSessionToParent.set(childSessionId, {
|
|
713
|
+
parentId: partSessionID
|
|
714
|
+
});
|
|
715
|
+
attachChildSessionToParentTask(
|
|
716
|
+
childSessionId,
|
|
717
|
+
partSessionID,
|
|
718
|
+
callID,
|
|
719
|
+
"part.metadata"
|
|
720
|
+
);
|
|
613
721
|
}
|
|
614
|
-
const existingIdx = calls.findIndex((c) => c.callId === callID);
|
|
615
|
-
const entry = { callId: callID, name: tool, args: JSON.stringify(input) };
|
|
616
|
-
if (existingIdx >= 0) calls[existingIdx] = entry;
|
|
617
|
-
else calls.push(entry);
|
|
618
|
-
state.toolCallMessageIds.set(callID, messageId);
|
|
619
722
|
}
|
|
620
|
-
} else if (
|
|
621
|
-
state.reasoningParts.set(messageId,
|
|
622
|
-
} else {
|
|
623
|
-
log(`message.part.updated: skipping part type=${String(part["type"])}`);
|
|
723
|
+
} else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
|
|
724
|
+
state.reasoningParts.set(messageId, partObj["text"]);
|
|
624
725
|
}
|
|
625
726
|
} else if (event.type === "message.updated") {
|
|
626
727
|
const msgInfo = props["info"];
|
|
627
|
-
if (
|
|
628
|
-
|
|
728
|
+
if (msgInfo == null || typeof msgInfo !== "object")
|
|
729
|
+
throw new Error("message.updated: props.info is required");
|
|
730
|
+
const info2 = msgInfo;
|
|
731
|
+
const role = info2["role"];
|
|
629
732
|
if (role !== "assistant") return;
|
|
630
|
-
const msgSessionID =
|
|
631
|
-
const messageId =
|
|
632
|
-
const time =
|
|
633
|
-
if (
|
|
733
|
+
const msgSessionID = info2["sessionID"];
|
|
734
|
+
const messageId = info2["id"];
|
|
735
|
+
const time = info2["time"];
|
|
736
|
+
if (msgSessionID == null || msgSessionID === "")
|
|
737
|
+
throw new Error("message.updated: info.sessionID is required");
|
|
738
|
+
if (messageId == null || messageId === "")
|
|
739
|
+
throw new Error("message.updated: info.id is required");
|
|
740
|
+
if (time == null || typeof time !== "object")
|
|
741
|
+
throw new Error("message.updated: info.time is required");
|
|
742
|
+
const timeObj = time;
|
|
743
|
+
if (timeObj["completed"] == null) return;
|
|
634
744
|
const state = sessions.get(msgSessionID);
|
|
635
|
-
if (!state || !state.
|
|
745
|
+
if (!state || !state.currentEventId || !state.currentRootSpan) return;
|
|
636
746
|
if (state.processedMessages.has(messageId)) return;
|
|
637
747
|
state.processedMessages.add(messageId);
|
|
638
|
-
const tokens =
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
const
|
|
643
|
-
const
|
|
748
|
+
const tokens = info2["tokens"];
|
|
749
|
+
if (tokens == null || typeof tokens !== "object")
|
|
750
|
+
throw new Error("message.updated: info.tokens is required");
|
|
751
|
+
const tokensObj = tokens;
|
|
752
|
+
const inputTokens = tokensObj["input"];
|
|
753
|
+
const outputTokens = tokensObj["output"];
|
|
754
|
+
const reasoningTokens = tokensObj["reasoning"];
|
|
755
|
+
if (typeof inputTokens !== "number" || typeof outputTokens !== "number")
|
|
756
|
+
throw new Error("message.updated: tokens.input and tokens.output must be numbers");
|
|
757
|
+
const providerID = info2["providerID"];
|
|
758
|
+
const modelID = info2["modelID"];
|
|
759
|
+
if (typeof providerID !== "string" || providerID === "" || typeof modelID !== "string" || modelID === "")
|
|
760
|
+
throw new Error(
|
|
761
|
+
"message.updated: providerID and modelID are required non-empty strings"
|
|
762
|
+
);
|
|
644
763
|
const modelName = `${providerID}/${modelID}`;
|
|
645
|
-
|
|
646
|
-
const outputText =
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
656
|
-
}
|
|
657
|
-
const llmSpan = traceShipper.startSpan({
|
|
658
|
-
name: modelName,
|
|
659
|
-
parent: state.currentTurnSpan.ids,
|
|
660
|
-
eventId: state.eventId,
|
|
661
|
-
attributes: llmAttrs
|
|
662
|
-
});
|
|
663
|
-
if (typeof time["created"] === "number") {
|
|
664
|
-
llmSpan.startTimeUnixNano = String(time["created"] * 1e6);
|
|
764
|
+
const finishReason = info2["finish"];
|
|
765
|
+
const outputText = state.outputParts.get(messageId);
|
|
766
|
+
const reasoningText = state.reasoningParts.get(messageId);
|
|
767
|
+
const isToolCallsCompletion = finishReason === "tool-calls";
|
|
768
|
+
const hasIntermediateReasoning = reasoningText !== void 0 && reasoningText.trim().length > 0;
|
|
769
|
+
if (outputText === void 0 && !(isToolCallsCompletion && hasIntermediateReasoning))
|
|
770
|
+
return;
|
|
771
|
+
if (isToolCallsCompletion && !hasIntermediateReasoning) {
|
|
772
|
+
await traceShipper.flush();
|
|
773
|
+
return;
|
|
665
774
|
}
|
|
666
|
-
const msgError =
|
|
667
|
-
const errorForSpan = msgError ?
|
|
775
|
+
const msgError = info2["error"];
|
|
776
|
+
const errorForSpan = msgError ? (() => {
|
|
777
|
+
var _a2, _b2;
|
|
778
|
+
const msg = (_b2 = (_a2 = msgError.data) == null ? void 0 : _a2.message) != null ? _b2 : msgError.name;
|
|
779
|
+
const name = msgError.name;
|
|
780
|
+
if (msg == null && name == null)
|
|
781
|
+
throw new Error("message.updated: error object must have name or data.message");
|
|
782
|
+
return `${msg != null ? msg : name}
|
|
668
783
|
|
|
669
|
-
type: ${
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
784
|
+
type: ${name != null ? name : "UnknownError"}`;
|
|
785
|
+
})() : void 0;
|
|
786
|
+
if (isToolCallsCompletion) {
|
|
787
|
+
if (reasoningText === void 0)
|
|
788
|
+
throw new Error("message.updated: reasoningParts missing for tool-call continuation");
|
|
789
|
+
const llmAttrs = [
|
|
790
|
+
attrString("ai.operationId", "generateText"),
|
|
791
|
+
attrString("ai.response.text", reasoningText),
|
|
792
|
+
attrString("gen_ai.system", providerID),
|
|
793
|
+
attrString("gen_ai.request.model", modelID),
|
|
794
|
+
attrInt("gen_ai.usage.input_tokens", inputTokens),
|
|
795
|
+
attrInt("gen_ai.usage.output_tokens", outputTokens)
|
|
796
|
+
];
|
|
797
|
+
if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
|
|
798
|
+
llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
799
|
+
}
|
|
800
|
+
const llmParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : state.currentRootSpan.ids;
|
|
801
|
+
const llmSpan = traceShipper.startSpan({
|
|
802
|
+
name: modelName,
|
|
803
|
+
parent: llmParent,
|
|
804
|
+
eventId: state.currentEventId,
|
|
805
|
+
attributes: llmAttrs
|
|
806
|
+
});
|
|
807
|
+
if (typeof timeObj["created"] === "number") {
|
|
808
|
+
llmSpan.startTimeUnixNano = String(timeObj["created"] * 1e6);
|
|
809
|
+
}
|
|
810
|
+
traceShipper.endSpan(llmSpan, { error: errorForSpan });
|
|
811
|
+
await traceShipper.flush();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (outputText === void 0) return;
|
|
815
|
+
const rootSpan = state.currentRootSpan;
|
|
816
|
+
if (state.parentId) {
|
|
817
|
+
const llmAttrs = [
|
|
818
|
+
attrString("ai.operationId", "generateText"),
|
|
819
|
+
attrString("ai.prompt", state.currentInput),
|
|
820
|
+
attrString("ai.response.text", outputText),
|
|
821
|
+
attrString("gen_ai.system", providerID),
|
|
822
|
+
attrString("gen_ai.request.model", modelID),
|
|
823
|
+
attrInt("gen_ai.usage.input_tokens", inputTokens),
|
|
824
|
+
attrInt("gen_ai.usage.output_tokens", outputTokens)
|
|
825
|
+
];
|
|
826
|
+
if (reasoningTokens != null && typeof reasoningTokens === "number" && reasoningTokens > 0) {
|
|
827
|
+
llmAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
828
|
+
}
|
|
829
|
+
if (!state.parentTaskSpanIds) {
|
|
830
|
+
traceShipper.endSpan(rootSpan, {
|
|
831
|
+
error: "Missing strict parent task for child session"
|
|
832
|
+
});
|
|
833
|
+
state.currentRootSpan = void 0;
|
|
834
|
+
state.currentEventId = void 0;
|
|
835
|
+
await traceShipper.flush();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const finalLlmSpan = traceShipper.startSpan({
|
|
839
|
+
name: modelName,
|
|
840
|
+
parent: state.parentTaskSpanIds,
|
|
841
|
+
eventId: state.currentEventId,
|
|
842
|
+
attributes: llmAttrs
|
|
843
|
+
});
|
|
844
|
+
if (typeof timeObj["created"] === "number") {
|
|
845
|
+
finalLlmSpan.startTimeUnixNano = String(timeObj["created"] * 1e6);
|
|
846
|
+
}
|
|
847
|
+
traceShipper.endSpan(finalLlmSpan, { error: errorForSpan });
|
|
848
|
+
traceShipper.endSpan(rootSpan, {
|
|
849
|
+
attributes: [
|
|
850
|
+
attrString("is_subagent", "true"),
|
|
851
|
+
attrString("parent_session_id", state.parentId)
|
|
852
|
+
],
|
|
853
|
+
error: errorForSpan
|
|
854
|
+
});
|
|
855
|
+
} else {
|
|
856
|
+
rootSpan.name = modelName;
|
|
857
|
+
const rootAttrs = [
|
|
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
|
+
rootAttrs.push(attrInt("gen_ai.usage.reasoning_tokens", reasoningTokens));
|
|
868
|
+
}
|
|
869
|
+
traceShipper.endSpan(rootSpan, {
|
|
870
|
+
attributes: rootAttrs,
|
|
871
|
+
error: errorForSpan
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
state.currentRootSpan = void 0;
|
|
875
|
+
const hasAssistantResponse = outputText.trim().length > 0;
|
|
876
|
+
if (!hasAssistantResponse) {
|
|
877
|
+
await traceShipper.flush();
|
|
878
|
+
state.currentEventId = void 0;
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (!state.parentId) {
|
|
882
|
+
await eventShipper.finish(state.currentEventId, {
|
|
883
|
+
userId: state.sessionId,
|
|
884
|
+
model: modelName,
|
|
885
|
+
output: outputText,
|
|
886
|
+
properties: {
|
|
887
|
+
turn_count: state.turnNumber,
|
|
888
|
+
tool_count: state.toolCount
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
await traceShipper.flush();
|
|
893
|
+
state.currentEventId = void 0;
|
|
682
894
|
} else if (event.type === "session.idle") {
|
|
683
895
|
if (!sessionID) return;
|
|
684
896
|
const state = sessions.get(String(sessionID));
|
|
685
897
|
if (!state) return;
|
|
686
|
-
if (state.
|
|
687
|
-
traceShipper.endSpan(state.
|
|
688
|
-
state.
|
|
898
|
+
if (state.currentRootSpan) {
|
|
899
|
+
traceShipper.endSpan(state.currentRootSpan);
|
|
900
|
+
state.currentRootSpan = void 0;
|
|
901
|
+
}
|
|
902
|
+
if (state.currentEventId) {
|
|
903
|
+
state.currentEventId = void 0;
|
|
904
|
+
}
|
|
905
|
+
await traceShipper.flush();
|
|
906
|
+
if (state.parentId) {
|
|
907
|
+
cleanupSessionParentMap(String(sessionID));
|
|
908
|
+
sessions.delete(String(sessionID));
|
|
909
|
+
} else {
|
|
689
910
|
}
|
|
690
|
-
await eventShipper.patch(state.eventId, {
|
|
691
|
-
output: state.currentOutput || void 0,
|
|
692
|
-
properties: {
|
|
693
|
-
turn_count: state.turnNumber,
|
|
694
|
-
tool_count: state.toolCount
|
|
695
|
-
}
|
|
696
|
-
});
|
|
697
|
-
log("Session idle \u2014 turn span closed", { sessionID });
|
|
698
911
|
} else if (event.type === "session.deleted") {
|
|
699
912
|
if (!sessionID) return;
|
|
700
913
|
const state = sessions.get(String(sessionID));
|
|
701
914
|
if (!state) return;
|
|
702
|
-
if (state.
|
|
703
|
-
traceShipper.endSpan(state.
|
|
704
|
-
state.
|
|
915
|
+
if (state.currentRootSpan) {
|
|
916
|
+
traceShipper.endSpan(state.currentRootSpan);
|
|
917
|
+
state.currentRootSpan = void 0;
|
|
705
918
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
properties: {
|
|
711
|
-
turn_count: state.turnNumber,
|
|
712
|
-
tool_count: state.toolCount
|
|
713
|
-
}
|
|
714
|
-
});
|
|
919
|
+
if (state.currentEventId) {
|
|
920
|
+
state.currentEventId = void 0;
|
|
921
|
+
}
|
|
922
|
+
cleanupSessionParentMap(String(sessionID));
|
|
715
923
|
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
716
924
|
sessions.delete(String(sessionID));
|
|
717
|
-
log("Session deleted \u2014 flushed", { sessionID });
|
|
718
925
|
} else if (event.type === "session.error") {
|
|
719
|
-
const errorSessionID = props["sessionID"]
|
|
720
|
-
if (
|
|
926
|
+
const errorSessionID = (_c = props["sessionID"]) != null ? _c : sessionID;
|
|
927
|
+
if (errorSessionID === void 0 || errorSessionID === "")
|
|
928
|
+
throw new Error("session.error: missing sessionID");
|
|
721
929
|
const state = sessions.get(String(errorSessionID));
|
|
722
930
|
if (!state) return;
|
|
723
931
|
const errorObj = props["error"];
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
const
|
|
932
|
+
if (errorObj == null) throw new Error("session.error: missing error object");
|
|
933
|
+
const errorName = errorObj.name;
|
|
934
|
+
const errorMessage = (_e = (_d = errorObj.data) == null ? void 0 : _d.message) != null ? _e : errorObj.name;
|
|
935
|
+
if (errorName == null && errorMessage == null)
|
|
936
|
+
throw new Error("session.error: error object must have name or data.message");
|
|
937
|
+
const errorStr = `${errorMessage != null ? errorMessage : errorName}
|
|
727
938
|
|
|
728
|
-
type: ${errorName}`;
|
|
729
|
-
if (state.
|
|
730
|
-
traceShipper.endSpan(state.
|
|
731
|
-
state.
|
|
939
|
+
type: ${errorName != null ? errorName : "UnknownError"}`;
|
|
940
|
+
if (state.currentRootSpan) {
|
|
941
|
+
traceShipper.endSpan(state.currentRootSpan, { error: errorStr });
|
|
942
|
+
state.currentRootSpan = void 0;
|
|
732
943
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
turn_count: state.turnNumber,
|
|
738
|
-
tool_count: state.toolCount
|
|
739
|
-
}
|
|
740
|
-
});
|
|
944
|
+
if (state.currentEventId) {
|
|
945
|
+
state.currentEventId = void 0;
|
|
946
|
+
}
|
|
947
|
+
cleanupSessionParentMap(String(errorSessionID));
|
|
741
948
|
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
742
949
|
sessions.delete(String(errorSessionID));
|
|
743
|
-
log("Session error handled", { errorSessionID, errorName });
|
|
744
|
-
} else {
|
|
745
|
-
log(`unhandled event: ${event.type}`);
|
|
746
950
|
}
|
|
747
951
|
} catch (err) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
level: "error",
|
|
752
|
-
message: `Error in event hook: ${err instanceof Error ? err.message : String(err)}`
|
|
753
|
-
}
|
|
754
|
-
})) == null ? void 0 : _j.catch) == null ? void 0 : _k.call(_j, () => {
|
|
755
|
-
});
|
|
952
|
+
console.log(
|
|
953
|
+
`[raindrop-ai/opencode-plugin] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`
|
|
954
|
+
);
|
|
756
955
|
}
|
|
757
956
|
},
|
|
758
957
|
// ------------------------------------------------------------------
|
|
759
|
-
// chat.message — user sent a message; start a new turn
|
|
958
|
+
// chat.message — user sent a message; start a new turn with a fresh event ID
|
|
760
959
|
// ------------------------------------------------------------------
|
|
761
960
|
"chat.message": async (messageInput, output) => {
|
|
762
|
-
var _a;
|
|
763
961
|
try {
|
|
764
962
|
const { sessionID } = messageInput;
|
|
765
|
-
|
|
766
|
-
if (!state)
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
963
|
+
let state = sessions.get(sessionID);
|
|
964
|
+
if (!state) {
|
|
965
|
+
state = createSessionState(sessionID);
|
|
966
|
+
sessions.set(sessionID, state);
|
|
967
|
+
}
|
|
968
|
+
applyChildSessionParentToState(sessionID);
|
|
969
|
+
if (state.currentRootSpan) {
|
|
970
|
+
traceShipper.endSpan(state.currentRootSpan);
|
|
971
|
+
state.currentRootSpan = void 0;
|
|
770
972
|
}
|
|
771
973
|
state.turnNumber++;
|
|
772
|
-
|
|
773
|
-
const parts =
|
|
974
|
+
if (output == null) throw new Error("chat.message: output is required");
|
|
975
|
+
const parts = output.parts;
|
|
976
|
+
if (parts == null) throw new Error("chat.message: output.parts is required");
|
|
774
977
|
const textParts = [];
|
|
775
978
|
const newAttachments = [];
|
|
776
979
|
for (const part of parts) {
|
|
@@ -781,84 +984,192 @@ type: ${errorName}`;
|
|
|
781
984
|
const filename = part["filename"];
|
|
782
985
|
const url = part["url"];
|
|
783
986
|
if (url && (mimeType == null ? void 0 : mimeType.startsWith("image/"))) {
|
|
987
|
+
if (filename === void 0 || filename === "")
|
|
988
|
+
throw new Error("chat.message: file part must have filename");
|
|
784
989
|
newAttachments.push({ type: "image", role: "input", name: filename, value: url });
|
|
785
990
|
}
|
|
786
991
|
}
|
|
787
992
|
}
|
|
788
993
|
const userText = textParts.join("\n");
|
|
789
994
|
state.currentInput = userText;
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
995
|
+
if (state.parentId) {
|
|
996
|
+
if (!state.parentTaskSpanIds || !state.parentEventContext) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
state.currentEventId = state.parentEventContext.eventId;
|
|
1000
|
+
state.currentRootSpan = traceShipper.startSpan({
|
|
1001
|
+
name: "Subagent",
|
|
1002
|
+
parent: state.parentTaskSpanIds,
|
|
1003
|
+
eventId: state.currentEventId,
|
|
1004
|
+
attributes: [
|
|
1005
|
+
attrString("workspace", worktree),
|
|
1006
|
+
attrString("directory", directory),
|
|
1007
|
+
attrString("hostname", hostname),
|
|
1008
|
+
attrString("os", os),
|
|
1009
|
+
attrString("is_subagent", "true"),
|
|
1010
|
+
attrString("parent_session_id", state.parentId)
|
|
1011
|
+
]
|
|
1012
|
+
});
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
state.currentEventId = generateId();
|
|
1016
|
+
state.currentRootSpan = traceShipper.startSpan({
|
|
1017
|
+
name: "ai.event",
|
|
1018
|
+
eventId: state.currentEventId,
|
|
1019
|
+
attributes: [
|
|
1020
|
+
attrString("workspace", worktree),
|
|
1021
|
+
attrString("directory", directory),
|
|
1022
|
+
attrString("hostname", hostname),
|
|
1023
|
+
attrString("os", os)
|
|
1024
|
+
]
|
|
801
1025
|
});
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1026
|
+
await eventShipper.patch(state.currentEventId, {
|
|
1027
|
+
isPending: true,
|
|
1028
|
+
userId: state.sessionId,
|
|
1029
|
+
convoId: state.sessionId,
|
|
1030
|
+
eventName: config.eventName,
|
|
1031
|
+
input: userText,
|
|
1032
|
+
...newAttachments.length > 0 ? { attachments: newAttachments } : {},
|
|
1033
|
+
properties: {
|
|
1034
|
+
workspace: worktree,
|
|
1035
|
+
directory,
|
|
1036
|
+
hostname,
|
|
1037
|
+
os,
|
|
1038
|
+
turn_count: state.turnNumber,
|
|
1039
|
+
tool_count: state.toolCount
|
|
1040
|
+
}
|
|
807
1041
|
});
|
|
808
1042
|
} catch (err) {
|
|
809
|
-
log("Error in chat.message hook", { error: String(err) });
|
|
810
1043
|
}
|
|
811
1044
|
},
|
|
812
1045
|
// ------------------------------------------------------------------
|
|
813
|
-
// tool.execute.before — record start time
|
|
1046
|
+
// tool.execute.before — record start time only (span created atomically in after)
|
|
1047
|
+
// For child sessions, tool spans are parented directly to the parent task span.
|
|
814
1048
|
// ------------------------------------------------------------------
|
|
815
1049
|
"tool.execute.before": async (toolInput, _output) => {
|
|
1050
|
+
var _a;
|
|
816
1051
|
try {
|
|
817
1052
|
const { tool, sessionID, callID } = toolInput;
|
|
818
1053
|
const state = sessions.get(sessionID);
|
|
819
|
-
if (!state || !state.
|
|
820
|
-
state.
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1054
|
+
if (!state || !state.currentEventId) return;
|
|
1055
|
+
const spanParent = state.parentId && state.parentTaskSpanIds ? state.parentTaskSpanIds : (_a = state.currentRootSpan) == null ? void 0 : _a.ids;
|
|
1056
|
+
if (!spanParent) return;
|
|
1057
|
+
const startTimeUnixNano = nowUnixNanoString();
|
|
1058
|
+
if (tool === "task") {
|
|
1059
|
+
const liveTaskSpan = traceShipper.startSpan({
|
|
1060
|
+
name: "ai.toolCall",
|
|
1061
|
+
parent: spanParent,
|
|
1062
|
+
eventId: state.currentEventId,
|
|
1063
|
+
attributes: [
|
|
1064
|
+
attrString("ai.operationId", "ai.toolCall"),
|
|
1065
|
+
attrString("ai.toolCall.name", tool),
|
|
1066
|
+
attrString("ai.toolCall.id", callID)
|
|
1067
|
+
]
|
|
1068
|
+
});
|
|
1069
|
+
liveTaskSpan.startTimeUnixNano = startTimeUnixNano;
|
|
1070
|
+
const ctx = {
|
|
1071
|
+
ids: liveTaskSpan.ids,
|
|
1072
|
+
eventContext: {
|
|
1073
|
+
eventId: state.currentEventId,
|
|
1074
|
+
userId: state.sessionId,
|
|
1075
|
+
convoId: state.sessionId
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
taskContexts.set(callKey(sessionID, callID), ctx);
|
|
1079
|
+
let running = runningTaskCallsBySession.get(sessionID);
|
|
1080
|
+
if (running == null) {
|
|
1081
|
+
running = /* @__PURE__ */ new Set();
|
|
1082
|
+
runningTaskCallsBySession.set(sessionID, running);
|
|
1083
|
+
}
|
|
1084
|
+
running.add(callID);
|
|
1085
|
+
resolvePendingChildrenForCall(sessionID, callID, "tool.execute.before");
|
|
1086
|
+
state.toolSpanStarts.set(callID, {
|
|
1087
|
+
startTimeUnixNano,
|
|
1088
|
+
parent: spanParent,
|
|
1089
|
+
eventId: state.currentEventId,
|
|
1090
|
+
name: tool,
|
|
1091
|
+
liveSpan: liveTaskSpan
|
|
1092
|
+
});
|
|
1093
|
+
} else {
|
|
1094
|
+
state.toolSpanStarts.set(callID, {
|
|
1095
|
+
startTimeUnixNano,
|
|
1096
|
+
parent: spanParent,
|
|
1097
|
+
eventId: state.currentEventId,
|
|
1098
|
+
name: tool
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
829
1101
|
} catch (err) {
|
|
830
|
-
log("Error in tool.execute.before hook", { error: String(err) });
|
|
831
1102
|
}
|
|
832
1103
|
},
|
|
833
1104
|
// ------------------------------------------------------------------
|
|
834
|
-
// tool.execute.after —
|
|
1105
|
+
// tool.execute.after — create tool span atomically with both start and end times
|
|
835
1106
|
// ------------------------------------------------------------------
|
|
836
1107
|
"tool.execute.after": async (toolInput, result) => {
|
|
837
|
-
var _a;
|
|
838
1108
|
try {
|
|
839
|
-
const { tool, sessionID, callID } = toolInput;
|
|
1109
|
+
const { tool, sessionID, callID, args } = toolInput;
|
|
840
1110
|
const state = sessions.get(sessionID);
|
|
841
|
-
if (!state) return;
|
|
1111
|
+
if (!state || !state.currentEventId) return;
|
|
842
1112
|
state.toolCount++;
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
state.
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1113
|
+
if (args === void 0 || args === null)
|
|
1114
|
+
throw new Error("tool.execute.after: args is required");
|
|
1115
|
+
const toolCallArgs = typeof args === "string" ? args : JSON.stringify(args);
|
|
1116
|
+
const startInfo = state.toolSpanStarts.get(callID);
|
|
1117
|
+
state.toolSpanStarts.delete(callID);
|
|
1118
|
+
const resultMetadata = result.metadata;
|
|
1119
|
+
const childSessionId = resultMetadata == null ? void 0 : resultMetadata["sessionId"];
|
|
1120
|
+
if (tool === "task" && childSessionId) {
|
|
1121
|
+
mapChildSessionToParent.set(childSessionId, { parentId: sessionID });
|
|
1122
|
+
attachChildSessionToParentTask(childSessionId, sessionID, callID, "tool.execute.after");
|
|
1123
|
+
}
|
|
1124
|
+
if (startInfo) {
|
|
1125
|
+
const endTimeUnixNano = nowUnixNanoString();
|
|
1126
|
+
if (!("output" in result) || result.output === void 0)
|
|
1127
|
+
throw new Error("tool.execute.after: result.output is required");
|
|
1128
|
+
let toolResult;
|
|
1129
|
+
try {
|
|
1130
|
+
toolResult = JSON.stringify(result.output);
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1133
|
+
throw new Error(`tool.execute.after: result.output must be JSON-serializable: ${msg}`);
|
|
1134
|
+
}
|
|
1135
|
+
if (startInfo.liveSpan) {
|
|
1136
|
+
traceShipper.endSpan(startInfo.liveSpan, {
|
|
1137
|
+
attributes: [
|
|
1138
|
+
attrString("ai.toolCall.args", toolCallArgs),
|
|
1139
|
+
attrString("ai.toolCall.result", toolResult)
|
|
1140
|
+
]
|
|
1141
|
+
});
|
|
1142
|
+
} else {
|
|
1143
|
+
traceShipper.createSpan({
|
|
1144
|
+
name: "ai.toolCall",
|
|
1145
|
+
parent: startInfo.parent,
|
|
1146
|
+
eventId: startInfo.eventId,
|
|
1147
|
+
startTimeUnixNano: startInfo.startTimeUnixNano,
|
|
1148
|
+
endTimeUnixNano,
|
|
1149
|
+
attributes: [
|
|
1150
|
+
attrString("ai.operationId", "ai.toolCall"),
|
|
1151
|
+
attrString("ai.toolCall.name", tool),
|
|
1152
|
+
attrString("ai.toolCall.id", callID),
|
|
1153
|
+
attrString("ai.toolCall.args", toolCallArgs),
|
|
1154
|
+
attrString("ai.toolCall.result", toolResult)
|
|
1155
|
+
]
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (tool === "task") {
|
|
1160
|
+
const running = runningTaskCallsBySession.get(sessionID);
|
|
1161
|
+
if (running) {
|
|
1162
|
+
running.delete(callID);
|
|
1163
|
+
if (running.size === 0) runningTaskCallsBySession.delete(sessionID);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (!state.parentId) {
|
|
1167
|
+
await eventShipper.patch(state.currentEventId, {
|
|
1168
|
+
userId: state.sessionId,
|
|
1169
|
+
properties: { tool_count: state.toolCount }
|
|
854
1170
|
});
|
|
855
1171
|
}
|
|
856
|
-
await eventShipper.patch(state.eventId, {
|
|
857
|
-
properties: { tool_count: state.toolCount }
|
|
858
|
-
});
|
|
859
|
-
log("Tool execute after", { tool, sessionID, callID });
|
|
860
1172
|
} catch (err) {
|
|
861
|
-
log("Error in tool.execute.after hook", { error: String(err) });
|
|
862
1173
|
}
|
|
863
1174
|
}
|
|
864
1175
|
};
|
|
@@ -867,25 +1178,16 @@ type: ${errorName}`;
|
|
|
867
1178
|
// src/index.ts
|
|
868
1179
|
async function plugin(input) {
|
|
869
1180
|
var _a;
|
|
870
|
-
const { client } = input;
|
|
871
1181
|
const config = loadConfig(input.directory);
|
|
872
1182
|
function appLog(level, message) {
|
|
873
|
-
|
|
874
|
-
(_b = (_a2 = client.app.log({
|
|
875
|
-
body: {
|
|
876
|
-
service: "raindrop-opencode",
|
|
877
|
-
level,
|
|
878
|
-
message
|
|
879
|
-
}
|
|
880
|
-
})) == null ? void 0 : _a2.catch) == null ? void 0 : _b.call(_a2, () => {
|
|
881
|
-
});
|
|
1183
|
+
console.log(`[raindrop-ai/opencode-plugin] [${level}] ${message}`);
|
|
882
1184
|
}
|
|
883
1185
|
if (!config.writeKey) {
|
|
884
1186
|
appLog(
|
|
885
1187
|
"warn",
|
|
886
1188
|
"RAINDROP_WRITE_KEY not set \u2014 Raindrop tracing disabled. Set it in your environment or in ~/.config/opencode/raindrop.json."
|
|
887
1189
|
);
|
|
888
|
-
return {
|
|
1190
|
+
return {};
|
|
889
1191
|
}
|
|
890
1192
|
if (config.debug) {
|
|
891
1193
|
appLog("info", `Raindrop tracing enabled (endpoint: ${config.endpoint})`);
|
|
@@ -902,12 +1204,11 @@ async function plugin(input) {
|
|
|
902
1204
|
});
|
|
903
1205
|
const worktree = (_a = input.worktree) != null ? _a : input.directory;
|
|
904
1206
|
const hooks = createHooks(
|
|
905
|
-
client,
|
|
906
1207
|
config,
|
|
907
1208
|
worktree,
|
|
908
1209
|
input.directory,
|
|
909
1210
|
eventShipper,
|
|
910
1211
|
traceShipper
|
|
911
1212
|
);
|
|
912
|
-
return
|
|
1213
|
+
return hooks;
|
|
913
1214
|
}
|