@sebastianandreasson/pi-autonomous-agents 0.15.0 → 0.15.1

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
  "name": "@sebastianandreasson/pi-autonomous-agents",
3
3
  "private": false,
4
- "version": "0.15.0",
4
+ "version": "0.15.1",
5
5
  "type": "module",
6
6
  "description": "Portable unattended PI harness for developer/tester/visual-review loops.",
7
7
  "license": "MIT",
@@ -118,6 +118,55 @@ function mergeSpanSummary(requestState) {
118
118
  }
119
119
  }
120
120
 
121
+ function getSpanSignal(requestState) {
122
+ const spans = Array.isArray(requestState?.spans) ? requestState.spans : []
123
+ let toolSpanCount = 0
124
+
125
+ for (const span of spans) {
126
+ const kind = String(span?.spanKind ?? '').trim()
127
+ if (kind === 'tool_call' || kind === 'tool_result') {
128
+ toolSpanCount += 1
129
+ }
130
+ }
131
+
132
+ return {
133
+ spanCount: spans.length,
134
+ toolSpanCount,
135
+ toolNameCount: requestState?.toolNames?.size ?? 0,
136
+ fileCount: requestState?.files?.size ?? 0,
137
+ }
138
+ }
139
+
140
+ function shouldPreferSnapshot(currentState, candidateState) {
141
+ const current = getSpanSignal(currentState)
142
+ const candidate = getSpanSignal(candidateState)
143
+
144
+ const currentHasToolContext = current.toolSpanCount > 0 || current.toolNameCount > 0 || current.fileCount > 0
145
+ const candidateHasToolContext = candidate.toolSpanCount > 0 || candidate.toolNameCount > 0 || candidate.fileCount > 0
146
+
147
+ if (candidateHasToolContext && !currentHasToolContext) {
148
+ return true
149
+ }
150
+
151
+ if (candidate.fileCount !== current.fileCount) {
152
+ return candidate.fileCount > current.fileCount
153
+ }
154
+
155
+ if (candidate.toolSpanCount !== current.toolSpanCount) {
156
+ return candidate.toolSpanCount > current.toolSpanCount
157
+ }
158
+
159
+ if (candidate.toolNameCount !== current.toolNameCount) {
160
+ return candidate.toolNameCount > current.toolNameCount
161
+ }
162
+
163
+ if (current.spanCount === 0 && candidate.spanCount > 0) {
164
+ return true
165
+ }
166
+
167
+ return false
168
+ }
169
+
121
170
  function applyAssistantMessage(requestState, message) {
122
171
  requestState.provider = String(message?.provider ?? requestState.provider ?? '').trim()
123
172
  requestState.model = String(message?.model ?? requestState.model ?? '').trim()
@@ -289,6 +338,23 @@ export function createRequestTelemetryExtension({ cwd = process.cwd() } = {}) {
289
338
  })
290
339
  }
291
340
 
341
+ function createComparableRequestState(baseState) {
342
+ return createRequestState({
343
+ sessionId: baseState.sessionId,
344
+ turnIndex: baseState.turnIndex,
345
+ startedAt: baseState.startedAt,
346
+ model: baseState.model,
347
+ metadata: {
348
+ runId: baseState.runId,
349
+ iteration: baseState.iteration,
350
+ phase: baseState.phase,
351
+ role: baseState.role,
352
+ kind: baseState.kind,
353
+ task: baseState.task,
354
+ },
355
+ })
356
+ }
357
+
292
358
  function createFallbackRequest() {
293
359
  const requestState = createProviderRequest()
294
360
 
@@ -417,7 +483,37 @@ export function createRequestTelemetryExtension({ cwd = process.cwd() } = {}) {
417
483
 
418
484
  pi.on('before_provider_request', (event) => {
419
485
  const requestState = createProviderRequest()
420
- if (!applyProviderPayloadSnapshot(requestState, event?.payload) && !applyContextSnapshot(requestState)) {
486
+ const providerApplied = applyProviderPayloadSnapshot(requestState, event?.payload)
487
+
488
+ const contextCandidate = createComparableRequestState(requestState)
489
+ const contextApplied = applyContextSnapshot(contextCandidate)
490
+ if (contextApplied && shouldPreferSnapshot(requestState, contextCandidate)) {
491
+ requestState.contextMessages = contextCandidate.contextMessages
492
+ requestState.spans = contextCandidate.spans
493
+ requestState.spanSource = contextCandidate.spanSource
494
+ requestState.contextMessageCount = contextCandidate.contextMessageCount
495
+ requestState.spanCount = contextCandidate.spanCount
496
+ requestState.textChars = contextCandidate.textChars
497
+ requestState.textBytes = contextCandidate.textBytes
498
+ requestState.toolNames = contextCandidate.toolNames
499
+ requestState.files = contextCandidate.files
500
+ }
501
+
502
+ const sessionHistoryCandidate = createComparableRequestState(requestState)
503
+ const sessionHistoryApplied = applySessionHistorySnapshot(sessionHistoryCandidate)
504
+ if (sessionHistoryApplied && shouldPreferSnapshot(requestState, sessionHistoryCandidate)) {
505
+ requestState.contextMessages = sessionHistoryCandidate.contextMessages
506
+ requestState.spans = sessionHistoryCandidate.spans
507
+ requestState.spanSource = sessionHistoryCandidate.spanSource
508
+ requestState.contextMessageCount = sessionHistoryCandidate.contextMessageCount
509
+ requestState.spanCount = sessionHistoryCandidate.spanCount
510
+ requestState.textChars = sessionHistoryCandidate.textChars
511
+ requestState.textBytes = sessionHistoryCandidate.textBytes
512
+ requestState.toolNames = sessionHistoryCandidate.toolNames
513
+ requestState.files = sessionHistoryCandidate.files
514
+ }
515
+
516
+ if (!providerApplied && requestState.spans.length === 0 && !contextApplied && !sessionHistoryApplied) {
421
517
  applySessionHistorySnapshot(requestState)
422
518
  }
423
519
  state.pendingRequests.push(requestState)
@@ -427,6 +427,84 @@ function parseStructuredTextArray(items) {
427
427
  return parts
428
428
  }
429
429
 
430
+ function normalizeProviderToolCalls(toolCalls) {
431
+ if (!Array.isArray(toolCalls)) {
432
+ return []
433
+ }
434
+
435
+ const parts = []
436
+ for (const item of toolCalls) {
437
+ const object = asObject(item)
438
+ const functionObject = asObject(object.function)
439
+ const id = normalizeString(object.id ?? object.call_id, '')
440
+ const name = normalizeString(
441
+ functionObject.name
442
+ ?? object.name,
443
+ ''
444
+ )
445
+ const argumentsValue = parseJsonLikeString(
446
+ functionObject.arguments
447
+ ?? object.arguments
448
+ )
449
+
450
+ if (name === '' && id === '' && argumentsValue === undefined) {
451
+ continue
452
+ }
453
+
454
+ parts.push({
455
+ type: 'toolCall',
456
+ id,
457
+ name,
458
+ arguments: argumentsValue,
459
+ })
460
+ }
461
+
462
+ return parts
463
+ }
464
+
465
+ function normalizeMessageContent(content) {
466
+ if (typeof content === 'string') {
467
+ return [{ type: 'text', text: content }]
468
+ }
469
+
470
+ if (Array.isArray(content)) {
471
+ return parseStructuredTextArray(content)
472
+ }
473
+
474
+ return []
475
+ }
476
+
477
+ function normalizeProviderRoleMessage(object) {
478
+ const role = normalizeString(object.role, '')
479
+ if (role === '') {
480
+ return null
481
+ }
482
+
483
+ if (role === 'tool') {
484
+ return {
485
+ role: 'toolResult',
486
+ toolCallId: normalizeString(object.tool_call_id ?? object.call_id ?? object.id, ''),
487
+ toolName: normalizeString(object.name, ''),
488
+ details: object,
489
+ content: [{
490
+ type: 'text',
491
+ text: typeof object.content === 'string' ? object.content : safeJson(object.content),
492
+ }],
493
+ }
494
+ }
495
+
496
+ const content = normalizeMessageContent(object.content)
497
+ const toolCalls = normalizeProviderToolCalls(object.tool_calls)
498
+
499
+ return {
500
+ role,
501
+ content: [...content, ...toolCalls],
502
+ toolCallId: normalizeString(object.toolCallId ?? object.tool_call_id ?? object.call_id, ''),
503
+ toolName: normalizeString(object.toolName ?? object.name, ''),
504
+ details: object.details,
505
+ }
506
+ }
507
+
430
508
  function convertProviderInputItemToMessage(item) {
431
509
  const object = asObject(item)
432
510
 
@@ -470,20 +548,7 @@ function convertProviderInputItemToMessage(item) {
470
548
  }
471
549
 
472
550
  if (object.role) {
473
- if (typeof object.content === 'string') {
474
- return {
475
- role: normalizeString(object.role, ''),
476
- content: [{ type: 'text', text: object.content }],
477
- }
478
- }
479
-
480
- return {
481
- role: normalizeString(object.role, ''),
482
- content: parseStructuredTextArray(object.content),
483
- toolCallId: normalizeString(object.toolCallId, ''),
484
- toolName: normalizeString(object.toolName, ''),
485
- details: object.details,
486
- }
551
+ return normalizeProviderRoleMessage(object)
487
552
  }
488
553
 
489
554
  const normalized = normalizeTextPart(object)
@@ -505,6 +570,22 @@ export function extractMessagesFromProviderPayload(payload) {
505
570
 
506
571
  if (Array.isArray(object.messages)) {
507
572
  return object.messages
573
+ .map((item) => {
574
+ if (typeof item === 'string') {
575
+ return {
576
+ role: 'user',
577
+ content: [{ type: 'text', text: item }],
578
+ }
579
+ }
580
+ if (!item || typeof item !== 'object') {
581
+ return null
582
+ }
583
+ if (item.role) {
584
+ return normalizeProviderRoleMessage(asObject(item))
585
+ }
586
+ return convertProviderInputItemToMessage(item)
587
+ })
588
+ .filter((message) => normalizeString(message?.role, '') !== '')
508
589
  }
509
590
 
510
591
  if (typeof object.input === 'string') {
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path'
2
2
  import process from 'node:process'
3
+ import { setMaxListeners } from 'node:events'
3
4
  import { pathToFileURL } from 'node:url'
4
5
  import {
5
6
  formatHeartbeatReason,
@@ -154,6 +155,16 @@ function applyRequestTelemetryEnv(request) {
154
155
  }
155
156
  }
156
157
 
158
+ function relaxAbortSignalListenerLimit(signal) {
159
+ if (!signal || typeof signal !== 'object') {
160
+ return
161
+ }
162
+
163
+ try {
164
+ setMaxListeners(0, signal)
165
+ } catch {}
166
+ }
167
+
157
168
  function deriveTokenAttributionKind({ activeToolName, pendingToolNames, pendingFiles, lastAssistantActivity }) {
158
169
  if (String(activeToolName ?? '').trim() !== '') {
159
170
  return 'tool_running'
@@ -484,6 +495,7 @@ export async function runSdkTurnWithPi(pi, request) {
484
495
  let tokenUsageEvents = 0
485
496
  let tokenUsage = createEmptyTokenUsage()
486
497
  let lastAssistantActivity = ''
498
+ let abortSignalLimitRelaxed = false
487
499
  const pendingToolNames = new Set()
488
500
  const pendingFiles = new Set()
489
501
  const events = []
@@ -569,6 +581,10 @@ export async function runSdkTurnWithPi(pi, request) {
569
581
  lastEventAt = Date.now()
570
582
 
571
583
  if (event.type === 'agent_start') {
584
+ if (!abortSignalLimitRelaxed) {
585
+ relaxAbortSignalListenerLimit(session?.agent?.signal)
586
+ abortSignalLimitRelaxed = true
587
+ }
572
588
  agentStarted = true
573
589
  emitLiveFeed(request, {
574
590
  type: 'agent_start',