@forloop-cc/forloop-opencode-plugin-planner 2.2.10 → 2.2.12
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@forloop-cc/forloop-opencode-plugin-planner",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.12",
|
|
5
5
|
"description": "ForLoop integration plugin for opencode — Sprint, story, and agent tools for the ForLoop platform",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -309,10 +309,12 @@ export class ForLoopAPIClient {
|
|
|
309
309
|
});
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
async addStoryComment(storyId: number, body: string): Promise<any> {
|
|
312
|
+
async addStoryComment(storyId: number, body: string, authorAgentKey?: string): Promise<any> {
|
|
313
|
+
const payload: Record<string, unknown> = { body };
|
|
314
|
+
if (authorAgentKey) payload.authorAgentKey = authorAgentKey;
|
|
313
315
|
return this.request(`/api/opencode/stories/${storyId}/comments`, {
|
|
314
316
|
method: 'POST',
|
|
315
|
-
body: JSON.stringify(
|
|
317
|
+
body: JSON.stringify(payload),
|
|
316
318
|
});
|
|
317
319
|
}
|
|
318
320
|
|
|
@@ -519,6 +521,10 @@ export class ForLoopAPIClient {
|
|
|
519
521
|
});
|
|
520
522
|
}
|
|
521
523
|
|
|
524
|
+
/**
|
|
525
|
+
* @deprecated Use writeConversationEvent() for new integrations.
|
|
526
|
+
* Kept for backward compatibility with existing plugin hooks.
|
|
527
|
+
*/
|
|
522
528
|
async recordMessage(data: MessageRecord): Promise<any> {
|
|
523
529
|
return this.request('/api/opencode/messages', {
|
|
524
530
|
method: 'POST',
|
|
@@ -533,6 +539,13 @@ export class ForLoopAPIClient {
|
|
|
533
539
|
});
|
|
534
540
|
}
|
|
535
541
|
|
|
542
|
+
async writeConversationEvent(data: WriteConversationEventInput): Promise<any> {
|
|
543
|
+
return this.request('/api/opencode/conversations/events', {
|
|
544
|
+
method: 'POST',
|
|
545
|
+
body: JSON.stringify(data),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
536
549
|
async sendStreamChunk(data: {
|
|
537
550
|
taskId: string;
|
|
538
551
|
trackingId: string;
|
|
@@ -554,6 +567,21 @@ export interface AgentSuggestionRequest {
|
|
|
554
567
|
query?: string;
|
|
555
568
|
}
|
|
556
569
|
|
|
570
|
+
export interface WriteConversationEventInput {
|
|
571
|
+
operation: 'append_turn' | 'start_turn' | 'complete_turn' | 'delete_turn';
|
|
572
|
+
sprintId: number;
|
|
573
|
+
targetAgent: string;
|
|
574
|
+
actor: { senderType: string; senderId: string };
|
|
575
|
+
conversationId?: string;
|
|
576
|
+
sessionId?: string;
|
|
577
|
+
turnId?: string;
|
|
578
|
+
messageId?: string;
|
|
579
|
+
requestId?: string;
|
|
580
|
+
userMessage?: string;
|
|
581
|
+
agentResponse?: string;
|
|
582
|
+
metadata?: Record<string, unknown>;
|
|
583
|
+
}
|
|
584
|
+
|
|
557
585
|
export interface ConversationHistoryParams {
|
|
558
586
|
sprintId?: number;
|
|
559
587
|
limit?: number;
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { ForLoopAPIClient } from '../capabilities/api-client';
|
|
2
|
-
import type {
|
|
2
|
+
import type { WriteConversationEventInput } from '../capabilities/api-client';
|
|
3
3
|
import { isLambdaExecution } from '../capabilities/config';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
|
|
7
|
+
interface PendingUserMessage {
|
|
8
|
+
messageId: string;
|
|
9
|
+
content: string;
|
|
10
|
+
agent: string;
|
|
11
|
+
model: any;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
function normalizeAgentKey(agent: string): string {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
return agent
|
|
18
|
-
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
19
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
20
|
-
.toLowerCase();
|
|
16
|
+
return agent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getActor(): { senderType: string; senderId: string } {
|
|
20
|
+
return {
|
|
21
|
+
senderType: process.env.FORLOOP_SENDER_TYPE || 'user',
|
|
22
|
+
senderId: process.env.FORLOOP_SENDER_ID || 'unknown',
|
|
23
|
+
};
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
function buildConversationId(sprintId: number, agent: string, sessionId: string): string {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
return `sprint:${sprintId}:agent:${agent}:${senderType}:${senderId}`;
|
|
27
|
+
const actor = getActor();
|
|
28
|
+
return `sprint:${sprintId}:agent:${agent}:${actor.senderType}:${actor.senderId}`;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
function readActiveSprintId(): number | null {
|
|
@@ -40,9 +42,11 @@ function readActiveSprintId(): number | null {
|
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
const userMessageBuffer = new Map<string, PendingUserMessage>();
|
|
46
|
+
|
|
47
|
+
async function writeConversationTurn(
|
|
44
48
|
client: ForLoopAPIClient,
|
|
45
|
-
|
|
49
|
+
input: WriteConversationEventInput,
|
|
46
50
|
attempts: number
|
|
47
51
|
): Promise<void> {
|
|
48
52
|
for (let i = 0; i < attempts; i++) {
|
|
@@ -50,20 +54,15 @@ async function recordAssistantMessage(
|
|
|
50
54
|
if (i > 0) {
|
|
51
55
|
await new Promise((resolve) => setTimeout(resolve, 600 * i));
|
|
52
56
|
}
|
|
53
|
-
const result = await client.
|
|
54
|
-
if (result?.
|
|
55
|
-
console.
|
|
56
|
-
return;
|
|
57
|
+
const result = await client.writeConversationEvent(input);
|
|
58
|
+
if (!result?.ok) {
|
|
59
|
+
console.warn(`[ForLoop] Write event returned not ok: ${result?.error || 'unknown'}`);
|
|
57
60
|
}
|
|
58
|
-
// After recording, try summarization (no-op if < 30 rounds)
|
|
59
|
-
client.summarizeConversation(data.sprintId).catch((err: Error) => {
|
|
60
|
-
console.warn('[ForLoop] Summarization trigger failed:', err.message);
|
|
61
|
-
});
|
|
62
61
|
return;
|
|
63
62
|
} catch (err: any) {
|
|
64
|
-
console.warn(`[ForLoop]
|
|
63
|
+
console.warn(`[ForLoop] Write event attempt ${i + 1}/${attempts} failed:`, err.message);
|
|
65
64
|
if (i === attempts - 1) {
|
|
66
|
-
console.warn('[ForLoop] Failed to
|
|
65
|
+
console.warn('[ForLoop] Failed to write conversation event after all retries');
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
}
|
|
@@ -85,7 +84,6 @@ export function createChatMessageHook(client: ForLoopAPIClient) {
|
|
|
85
84
|
const { message, parts } = output;
|
|
86
85
|
const content = extractTextFromParts(parts);
|
|
87
86
|
|
|
88
|
-
// Log incoming user message for debug visibility (Lambda)
|
|
89
87
|
if (content && messageID) {
|
|
90
88
|
console.log(`[ForLoop] REQUEST — user message (${content.length} chars)`, {
|
|
91
89
|
sprintId,
|
|
@@ -95,25 +93,15 @@ export function createChatMessageHook(client: ForLoopAPIClient) {
|
|
|
95
93
|
});
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
// Only record messages with a model (indicates actual LLM processing,
|
|
99
|
-
// not system events like context compaction or summary generation)
|
|
100
96
|
if (!model || !model.providerID) return;
|
|
97
|
+
if (!content) return;
|
|
101
98
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// Fire-and-forget: don't await - must not block the LLM from receiving the message
|
|
105
|
-
client.recordMessage({
|
|
106
|
-
sprintId,
|
|
107
|
-
sessionId: sessionID,
|
|
99
|
+
userMessageBuffer.set(sessionID, {
|
|
108
100
|
messageId: messageID || message.id,
|
|
109
|
-
conversationId,
|
|
110
|
-
role: 'user',
|
|
111
101
|
content,
|
|
112
|
-
agent: agent
|
|
102
|
+
agent: agent || 'unknown',
|
|
113
103
|
model,
|
|
114
104
|
timestamp: message.time?.created ?? Date.now(),
|
|
115
|
-
}).catch((err: Error) => {
|
|
116
|
-
console.warn('[ForLoop] Failed to record user message:', err.message);
|
|
117
105
|
});
|
|
118
106
|
};
|
|
119
107
|
}
|
|
@@ -198,21 +186,36 @@ export function createEventHook(client: ForLoopAPIClient) {
|
|
|
198
186
|
preview: bufferedText.substring(0, 500),
|
|
199
187
|
});
|
|
200
188
|
|
|
189
|
+
const pendingUserMsg = userMessageBuffer.get(info.sessionID);
|
|
190
|
+
userMessageBuffer.delete(info.sessionID);
|
|
191
|
+
|
|
192
|
+
if (!pendingUserMsg) {
|
|
193
|
+
console.log('[ForLoop] Skipping write — no buffered user message for this session (stream handler handles the save)');
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
201
197
|
const conversationId = buildConversationId(
|
|
202
198
|
sprintId,
|
|
203
199
|
info.agent || 'unknown',
|
|
204
200
|
info.sessionID
|
|
205
201
|
);
|
|
206
|
-
|
|
202
|
+
writeConversationTurn(client, {
|
|
203
|
+
operation: 'append_turn',
|
|
207
204
|
sprintId,
|
|
205
|
+
targetAgent: info.agent ? normalizeAgentKey(info.agent) : 'unknown',
|
|
206
|
+
actor: getActor(),
|
|
207
|
+
conversationId,
|
|
208
208
|
sessionId: info.sessionID,
|
|
209
209
|
messageId: info.id,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
210
|
+
userMessage: pendingUserMsg.content,
|
|
211
|
+
agentResponse: bufferedText,
|
|
212
|
+
metadata: {
|
|
213
|
+
source: 'opencode-plugin',
|
|
214
|
+
agent: info.agent ? normalizeAgentKey(info.agent) : null,
|
|
215
|
+
model: info.model || null,
|
|
216
|
+
userMessageId: pendingUserMsg.messageId || null,
|
|
217
|
+
recordedAt: info.time?.created ?? Date.now(),
|
|
218
|
+
},
|
|
216
219
|
}, 3);
|
|
217
220
|
break;
|
|
218
221
|
}
|
|
@@ -220,8 +223,17 @@ export function createEventHook(client: ForLoopAPIClient) {
|
|
|
220
223
|
const { sessionID, messageID } = event.properties || {};
|
|
221
224
|
if (!sessionID || !messageID) break;
|
|
222
225
|
textBuffer.delete(messageID);
|
|
223
|
-
|
|
224
|
-
|
|
226
|
+
const conversationId = buildConversationId(sprintId, 'unknown', sessionID);
|
|
227
|
+
client.writeConversationEvent({
|
|
228
|
+
operation: 'delete_turn',
|
|
229
|
+
sprintId,
|
|
230
|
+
targetAgent: 'forLoopPlanner',
|
|
231
|
+
actor: getActor(),
|
|
232
|
+
conversationId,
|
|
233
|
+
messageId: messageID,
|
|
234
|
+
metadata: { source: 'opencode-plugin' },
|
|
235
|
+
}).catch((err: Error) => {
|
|
236
|
+
console.warn('[ForLoop] Failed to delete conversation turn:', err.message);
|
|
225
237
|
});
|
|
226
238
|
break;
|
|
227
239
|
}
|
|
@@ -243,21 +255,36 @@ export function createEventHook(client: ForLoopAPIClient) {
|
|
|
243
255
|
if (!info || info.role !== 'assistant') return;
|
|
244
256
|
const bufferedText = textBuffer.get(info.id);
|
|
245
257
|
if (!bufferedText) return;
|
|
258
|
+
const pendingUserMsg = userMessageBuffer.get(info.sessionID);
|
|
259
|
+
userMessageBuffer.delete(info.sessionID);
|
|
260
|
+
|
|
261
|
+
if (!pendingUserMsg) {
|
|
262
|
+
console.log('[ForLoop] Skipping write — no buffered user message for this session');
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
246
266
|
const conversationId = buildConversationId(
|
|
247
267
|
sprintId,
|
|
248
268
|
info.agent || 'unknown',
|
|
249
269
|
info.sessionID
|
|
250
270
|
);
|
|
251
|
-
|
|
271
|
+
writeConversationTurn(client, {
|
|
272
|
+
operation: 'append_turn',
|
|
252
273
|
sprintId,
|
|
274
|
+
targetAgent: info.agent ? normalizeAgentKey(info.agent) : 'unknown',
|
|
275
|
+
actor: getActor(),
|
|
276
|
+
conversationId,
|
|
253
277
|
sessionId: info.sessionID,
|
|
254
278
|
messageId: info.id,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
279
|
+
userMessage: pendingUserMsg.content,
|
|
280
|
+
agentResponse: bufferedText,
|
|
281
|
+
metadata: {
|
|
282
|
+
source: 'opencode-plugin',
|
|
283
|
+
agent: info.agent ? normalizeAgentKey(info.agent) : null,
|
|
284
|
+
model: info.model || null,
|
|
285
|
+
userMessageId: pendingUserMsg.messageId || null,
|
|
286
|
+
recordedAt: info.time?.created ?? Date.now(),
|
|
287
|
+
},
|
|
261
288
|
}, 3);
|
|
262
289
|
break;
|
|
263
290
|
}
|
|
@@ -265,8 +292,17 @@ export function createEventHook(client: ForLoopAPIClient) {
|
|
|
265
292
|
const { sessionID, messageID } = event.properties || {};
|
|
266
293
|
if (!sessionID || !messageID) return;
|
|
267
294
|
textBuffer.delete(messageID);
|
|
268
|
-
|
|
269
|
-
|
|
295
|
+
const conversationId = buildConversationId(sprintId, 'unknown', sessionID);
|
|
296
|
+
client.writeConversationEvent({
|
|
297
|
+
operation: 'delete_turn',
|
|
298
|
+
sprintId,
|
|
299
|
+
targetAgent: 'forLoopPlanner',
|
|
300
|
+
actor: getActor(),
|
|
301
|
+
conversationId,
|
|
302
|
+
messageId: messageID,
|
|
303
|
+
metadata: { source: 'opencode-plugin' },
|
|
304
|
+
}).catch((err: Error) => {
|
|
305
|
+
console.warn('[ForLoop] Failed to delete conversation turn:', err.message);
|
|
270
306
|
});
|
|
271
307
|
break;
|
|
272
308
|
}
|