@bluecopa/harness 0.1.0-snapshot.27 → 0.1.0-snapshot.29
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 +1 -1
- package/src/arc/arc-loop.ts +66 -21
package/package.json
CHANGED
package/src/arc/arc-loop.ts
CHANGED
|
@@ -78,6 +78,8 @@ export class ArcLoop {
|
|
|
78
78
|
private readonly traceWriter: ((event: TraceEvent) => void) | undefined;
|
|
79
79
|
private readonly tracedRunning = new Set<string>();
|
|
80
80
|
private readonly reportedCompletions = new Set<string>();
|
|
81
|
+
/** Maps normalized action text → process ID for dedup. */
|
|
82
|
+
private readonly actionIndex = new Map<string, string>();
|
|
81
83
|
private readonly processListeners: Promise<void>[] = [];
|
|
82
84
|
private readonly skillRouter: SkillRouter | undefined;
|
|
83
85
|
private skillSummaries: SkillSummary[] | null = null;
|
|
@@ -266,28 +268,71 @@ export class ArcLoop {
|
|
|
266
268
|
|
|
267
269
|
if (call.toolName === 'Thread') {
|
|
268
270
|
const request = this.toProcessRequest(call.args);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
result: resultText,
|
|
279
|
-
}
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
271
|
+
|
|
272
|
+
// Require profile when processProfiles are configured
|
|
273
|
+
const hasProfiles = this.config.processProfiles && Object.keys(this.config.processProfiles).length > 0;
|
|
274
|
+
if (hasProfiles && !request.profile) {
|
|
275
|
+
const names = Object.keys(this.config.processProfiles!).map(n => `"${n}"`).join(', ');
|
|
276
|
+
const resultText = `ERROR: profile parameter is required. Available profiles: ${names}. Re-call Thread with a profile.`;
|
|
277
|
+
toolResultMessages.push({
|
|
278
|
+
role: 'tool',
|
|
279
|
+
content: resultText,
|
|
280
|
+
toolResults: [{ toolCallId, toolName: 'Thread', result: resultText }],
|
|
281
|
+
});
|
|
282
|
+
} else if (hasProfiles && request.profile && !this.config.processProfiles![request.profile]) {
|
|
283
|
+
const names = Object.keys(this.config.processProfiles!).map(n => `"${n}"`).join(', ');
|
|
284
|
+
const resultText = `ERROR: unknown profile "${request.profile}". Available profiles: ${names}. Re-call Thread with a valid profile.`;
|
|
285
|
+
toolResultMessages.push({
|
|
286
|
+
role: 'tool',
|
|
287
|
+
content: resultText,
|
|
288
|
+
toolResults: [{ toolCallId, toolName: 'Thread', result: resultText }],
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
|
|
292
|
+
// Dedup: reject re-dispatch of identical actions unless the previous attempt failed
|
|
293
|
+
const normAction = request.action.trim().replace(/\s+/g, ' ');
|
|
294
|
+
const existingId = this.actionIndex.get(normAction);
|
|
295
|
+
const existing = existingId ? this.processes.get(existingId) : undefined;
|
|
296
|
+
|
|
297
|
+
if (existing && (existing.status === 'running' || existing.status === 'pending')) {
|
|
298
|
+
const resultText = `DUPLICATE — thread already running for this action (process ${existing.id}). Wait for it to complete.`;
|
|
299
|
+
toolResultMessages.push({
|
|
300
|
+
role: 'tool',
|
|
301
|
+
content: resultText,
|
|
302
|
+
toolResults: [{ toolCallId, toolName: 'Thread', result: resultText }],
|
|
303
|
+
});
|
|
304
|
+
} else if (existing && existing.status === 'completed' && existing.result) {
|
|
305
|
+
const ep = existing.result.episode;
|
|
306
|
+
const resultText = `DUPLICATE — this action already completed (process ${existing.id}, episodeId: ${ep.id}). Use contextEpisodeIds: ["${ep.id}"] to reference it.`;
|
|
307
|
+
toolResultMessages.push({
|
|
308
|
+
role: 'tool',
|
|
309
|
+
content: resultText,
|
|
310
|
+
toolResults: [{ toolCallId, toolName: 'Thread', result: resultText }],
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
// New dispatch (or retry of a failed action)
|
|
314
|
+
const proc = this.dispatch(request, signal);
|
|
315
|
+
this.actionIndex.set(normAction, proc.id);
|
|
316
|
+
this.trace({ type: 'process_created', id: proc.id, status: 'pending' });
|
|
317
|
+
const resultText = `Process ${proc.id} dispatched: "${request.action}" (model: ${request.model ?? 'medium'})`;
|
|
318
|
+
toolResultMessages.push({
|
|
319
|
+
role: 'tool',
|
|
320
|
+
content: resultText,
|
|
321
|
+
toolResults: [{ toolCallId, toolName: 'Thread', result: resultText }],
|
|
322
|
+
});
|
|
323
|
+
const dispatchEvent: ArcEvent = {
|
|
324
|
+
type: 'process_dispatched',
|
|
325
|
+
id: proc.id,
|
|
326
|
+
action: request.action,
|
|
327
|
+
model: proc.model,
|
|
328
|
+
};
|
|
329
|
+
if (request.label != null) {
|
|
330
|
+
(dispatchEvent as { label?: string }).label = request.label;
|
|
331
|
+
}
|
|
332
|
+
yield dispatchEvent;
|
|
289
333
|
}
|
|
290
|
-
|
|
334
|
+
|
|
335
|
+
} // end profile validation else
|
|
291
336
|
} else if (call.toolName === 'Check') {
|
|
292
337
|
const proc = this.processes.get(String(call.args.id));
|
|
293
338
|
let resultText: string;
|