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