deepseek-agent-acp 0.2.1 → 0.2.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.js CHANGED
@@ -147,6 +147,7 @@ class Session {
147
147
  checkpoint;
148
148
  abortController = null;
149
149
  activePromptId = null;
150
+ activeTurnHandle = null;
150
151
  constructor(sessionId) {
151
152
  this.sessionId = sessionId;
152
153
  const sysPrompt = getSystemPrompt(MODEL);
@@ -218,6 +219,11 @@ async function handleRpcMessage(rpc) {
218
219
  streaming: true,
219
220
  tools: true,
220
221
  sessionCancellation: true
222
+ },
223
+ _meta: {
224
+ steering: {
225
+ supported: true
226
+ }
221
227
  }
222
228
  });
223
229
  break;
@@ -244,6 +250,10 @@ async function handleRpcMessage(rpc) {
244
250
  await handleSessionPrompt(id, params);
245
251
  break;
246
252
  }
253
+ case '_session/steering': {
254
+ handleSessionSteering(id, params);
255
+ break;
256
+ }
247
257
  default: {
248
258
  console.warn(`Unhandled method: ${method}`);
249
259
  if (id !== undefined && id !== null) {
@@ -335,6 +345,7 @@ async function handleSessionPrompt(id, params) {
335
345
  session.messages.push({ role: 'user', content: promptText });
336
346
  const nostrMeta = parseNostrContext(promptText);
337
347
  const turnHandle = telemetryService?.startTurnSpan(sessionId, promptText, MODEL, nostrMeta);
348
+ session.activeTurnHandle = turnHandle ?? null;
338
349
  const maxIterations = getIterationLimit(MODEL);
339
350
  let iteration = 0;
340
351
  let lastToolError = null;
@@ -659,7 +670,41 @@ async function handleSessionPrompt(id, params) {
659
670
  finally {
660
671
  session.abortController = null;
661
672
  session.activePromptId = null;
673
+ session.activeTurnHandle = null;
674
+ }
675
+ }
676
+ /**
677
+ * Handle ACP _session/steering requests (mid-turn follow-up injection).
678
+ */
679
+ function handleSessionSteering(id, params) {
680
+ const sessionId = params?.sessionId || '';
681
+ const session = sessionId ? sessions.get(sessionId) : undefined;
682
+ const promptText = extractPromptText(params);
683
+ if (!session || !session.activePromptId) {
684
+ // Session is idle or has no running turn
685
+ sendResponse(id, {
686
+ outcome: 'promptRequired',
687
+ reason: 'noRunningTurn'
688
+ });
689
+ return;
690
+ }
691
+ // Active turn in flight: parse nostr context and record steer child span
692
+ const nostrMeta = parseNostrContext(promptText);
693
+ if (session.activeTurnHandle) {
694
+ session.activeTurnHandle.recordSteerSpan({
695
+ eventId: nostrMeta.eventId,
696
+ text: promptText,
697
+ senderName: nostrMeta.senderName,
698
+ senderPubkey: nostrMeta.senderPubkey,
699
+ priority: 'now'
700
+ });
662
701
  }
702
+ // Inject into active turn messages for the model's next iteration
703
+ const steerHeader = '[New message — arrived while you were working]';
704
+ const steerFormatted = `${steerHeader}\n${promptText}`;
705
+ session.messages.push({ role: 'user', content: steerFormatted });
706
+ console.info(`[STEER] Injected steered message into active session ${sessionId} (event: ${nostrMeta.eventId ?? 'unknown'})`);
707
+ sendResponse(id, { outcome: 'injected' });
663
708
  }
664
709
  // ============================================================================
665
710
  // 6. PROCESS STDIN LISTENER
@@ -44,18 +44,33 @@ export interface NostrMetadata {
44
44
  */
45
45
  export declare function parseNostrContext(rawText: unknown): NostrMetadata;
46
46
  /**
47
- * Extract the posted Buzz event ID from a reply tool result.
48
- * buzz-thread reply tools return `posted (N characters) · event_id <64 hex>`.
47
+ * Regex matching posted Nostr event ID across text, JSON, and escaped JSON formats:
48
+ * - buzz messages send / relay stdout: {"accepted":true,"event_id":"<64 hex>",...}
49
+ * - escaped JSON stdout: {\"accepted\":true,\"event_id\":\"<64 hex>\"}
50
+ * - reply tools: `posted (N characters) · event_id <64 hex>`
51
+ * - shell assignments: `event_id=<64 hex>` or `event_id: <64 hex>`
52
+ */
53
+ export declare const POSTED_OUTPUT_PATTERN: RegExp;
54
+ /**
55
+ * Extract the posted Buzz event ID from a reply tool result or shell output.
49
56
  */
50
57
  export declare function extractPostedEventId(output: unknown): string | undefined;
51
58
  export interface LlmSpanHandle {
52
59
  recordUsage(usage: DeepSeekUsage): void;
53
60
  end(outputValue?: string, errorMessage?: string): void;
54
61
  }
62
+ export interface SteerSpanParams {
63
+ eventId?: string;
64
+ text: string;
65
+ senderName?: string;
66
+ senderPubkey?: string;
67
+ priority?: 'now' | 'later';
68
+ }
55
69
  export interface TurnSpanHandle {
56
70
  startLlmSpan(modelName: string, iteration?: number): LlmSpanHandle;
57
71
  startToolSpan(toolCallId: string, toolName: string, toolParams: unknown): void;
58
72
  endToolSpan(toolCallId: string, output?: unknown, isError?: boolean, toolName?: string): void;
73
+ recordSteerSpan(params: SteerSpanParams): void;
59
74
  recordUsage(usage: DeepSeekUsage): void;
60
75
  end(status: 'OK' | 'ERROR' | 'CANCELLED', outputOrError?: string, failureKind?: string, errorMessage?: string): void;
61
76
  }
package/dist/telemetry.js CHANGED
@@ -195,26 +195,43 @@ export function parseNostrContext(rawText) {
195
195
  return result;
196
196
  }
197
197
  /**
198
- * Extract the posted Buzz event ID from a reply tool result.
199
- * buzz-thread reply tools return `posted (N characters) · event_id <64 hex>`.
198
+ * Regex matching posted Nostr event ID across text, JSON, and escaped JSON formats:
199
+ * - buzz messages send / relay stdout: {"accepted":true,"event_id":"<64 hex>",...}
200
+ * - escaped JSON stdout: {\"accepted\":true,\"event_id\":\"<64 hex>\"}
201
+ * - reply tools: `posted (N characters) · event_id <64 hex>`
202
+ * - shell assignments: `event_id=<64 hex>` or `event_id: <64 hex>`
203
+ */
204
+ export const POSTED_OUTPUT_PATTERN = /\bevent_id\\?["']?\s*[:=]?\s*\\?["']?([0-9a-f]{64})(?![0-9a-f])/i;
205
+ /**
206
+ * Extract the posted Buzz event ID from a reply tool result or shell output.
200
207
  */
201
208
  export function extractPostedEventId(output) {
202
209
  let text = '';
203
210
  if (typeof output === 'string') {
204
211
  text = output;
212
+ try {
213
+ const parsed = JSON.parse(output);
214
+ if (parsed && typeof parsed.stdout === 'string') {
215
+ text += ` ${parsed.stdout}`;
216
+ }
217
+ }
218
+ catch {
219
+ // Ignored
220
+ }
205
221
  }
206
222
  else if (output && typeof output === 'object') {
223
+ const rec = output;
207
224
  text = JSON.stringify(output);
225
+ if (typeof rec.stdout === 'string') {
226
+ text += ` ${rec.stdout}`;
227
+ }
208
228
  }
209
229
  if (!text) {
210
230
  return undefined;
211
231
  }
212
- const match = /\bevent_id\s+([0-9a-f]{64})\b/i.exec(text.slice(0, 4096));
232
+ const match = POSTED_OUTPUT_PATTERN.exec(text.slice(0, 8192));
213
233
  return match?.[1] ? match[1].toLowerCase() : undefined;
214
234
  }
215
- function isReplyTool(toolName) {
216
- return toolName === 'mcp__buzz-thread__reply' || toolName === 'reply' || toolName.endsWith('reply');
217
- }
218
235
  // ============================================================================
219
236
  // 4. FLUSH COALESCING & ERROR THROTTLING
220
237
  // ============================================================================
@@ -309,7 +326,16 @@ export function initTelemetry(options) {
309
326
  const tracer = trace.getTracer('deepseek-agent-acp', getPackageVersion());
310
327
  return {
311
328
  startTurnSpan(sessionId, promptText, modelName, nostrMeta) {
312
- const sanitizedPrompt = truncatePayload(redactSecrets(promptText), 8192);
329
+ // Extract the event message block if present for cleaner prompt preview in Phoenix & buzz-trace
330
+ let promptForSpan = promptText;
331
+ if (promptText.includes('\n') && promptText.includes('Event ID:')) {
332
+ const blocks = promptText.split(/\n{2,}/);
333
+ const eventBlock = blocks.find((b) => b.includes('Event ID:'));
334
+ if (eventBlock) {
335
+ promptForSpan = eventBlock;
336
+ }
337
+ }
338
+ const sanitizedPrompt = truncatePayload(redactSecrets(promptForSpan), 8192);
313
339
  const resolvedSessionId = nostrMeta?.threadRootId ?? sessionId;
314
340
  const rootSpan = tracer.startSpan('agent_turn', {
315
341
  attributes: {
@@ -429,18 +455,26 @@ export function initTelemetry(options) {
429
455
  }
430
456
  return;
431
457
  }
432
- const { span: toolSpan, toolName: resolvedToolName } = entry;
458
+ const { span: toolSpan } = entry;
433
459
  const outputBytes = computeOutputBytes(output);
434
460
  toolSpan.setAttribute('tool.output_bytes', outputBytes);
435
- // Extract posted_event_id for reply tools
436
- if (!isError && isReplyTool(resolvedToolName)) {
461
+ // Extract posted_event_id for any posting tool (reply, buzz-thread__reply, shell, etc.)
462
+ let hasPostedId = false;
463
+ if (!isError) {
437
464
  const postedId = extractPostedEventId(output);
438
465
  if (postedId) {
466
+ hasPostedId = true;
439
467
  toolSpan.setAttribute('nostr.posted_event_id', postedId);
440
468
  rootSpan.setAttribute('nostr.posted_event_id', postedId);
469
+ // Ensure output.value and tool.output are captured on the posting span for trace fidelity
470
+ const rawStr = typeof output === 'string' ? output : JSON.stringify(output);
471
+ const sanitizedOut = truncatePayload(redactSecrets(rawStr), 8192);
472
+ toolSpan.setAttribute('output.value', sanitizedOut);
473
+ toolSpan.setAttribute('tool.output', sanitizedOut);
474
+ toolSpan.setAttribute('output.mime_type', 'text/plain');
441
475
  }
442
476
  }
443
- if (process.env.OTEL_CAPTURE_TOOL_OUTPUT === 'true' && output !== undefined) {
477
+ if (process.env.OTEL_CAPTURE_TOOL_OUTPUT === 'true' && output !== undefined && !hasPostedId) {
444
478
  const sanitizedOut = truncatePayload(redactSecrets(output), 8192);
445
479
  toolSpan.setAttribute('output.value', sanitizedOut);
446
480
  toolSpan.setAttribute('output.mime_type', 'text/plain');
@@ -458,6 +492,32 @@ export function initTelemetry(options) {
458
492
  toolSpan.end();
459
493
  activeToolSpans.delete(toolCallId);
460
494
  },
495
+ recordSteerSpan(steerParams) {
496
+ const sanitizedText = truncatePayload(redactSecrets(steerParams.text), 8192);
497
+ const steerSpan = tracer.startSpan('steer', {
498
+ attributes: {
499
+ 'openinference.span.kind': 'CHAIN',
500
+ 'session.id': resolvedSessionId,
501
+ ...(steerParams.eventId ? { 'nostr.event_id': steerParams.eventId.toLowerCase() } : {}),
502
+ 'input.value': sanitizedText,
503
+ 'input.mime_type': 'text/plain',
504
+ ...(steerParams.senderName ? { 'nostr.sender_name': steerParams.senderName } : {}),
505
+ ...(steerParams.senderPubkey ? { 'nostr.sender_pubkey': steerParams.senderPubkey.toLowerCase() } : {}),
506
+ 'steer.priority': steerParams.priority ?? 'now',
507
+ 'steer.delivered': true,
508
+ 'steer.end_reason': 'echo'
509
+ }
510
+ }, rootContext);
511
+ steerSpan.setStatus({ code: SpanStatusCode.OK });
512
+ steerSpan.end();
513
+ rootSpan.addEvent('steer', {
514
+ ...(steerParams.eventId ? { 'nostr.event_id': steerParams.eventId.toLowerCase() } : {}),
515
+ 'input.value': sanitizedText,
516
+ ...(steerParams.senderName ? { 'nostr.sender_name': steerParams.senderName } : {}),
517
+ 'steer.priority': steerParams.priority ?? 'now',
518
+ 'steer.delivered': true
519
+ });
520
+ },
461
521
  recordUsage(usage) {
462
522
  const promptTokens = usage.prompt_tokens || 0;
463
523
  const completionTokens = usage.completion_tokens || 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-agent-acp",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Agent Client Protocol (ACP) adapter for DeepSeek reasoning and chat models with Model Context Protocol (MCP) tool execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",