@poncho-ai/harness 0.28.2 → 0.29.0
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/.turbo/turbo-build.log +5 -5
- package/.turbo/turbo-lint.log +6 -0
- package/.turbo/turbo-test.log +34 -0
- package/CHANGELOG.md +21 -0
- package/dist/index.d.ts +23 -5
- package/dist/index.js +294 -90
- package/package.json +4 -2
- package/src/config.ts +4 -1
- package/src/harness.ts +211 -19
- package/src/skill-tools.ts +6 -3
- package/src/state.ts +5 -0
- package/src/telemetry.ts +20 -10
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.29.0 build /Users/cesar/Dev/latitude/poncho-ai/packages/harness
|
|
3
3
|
> node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[embed-docs] Generated poncho-docs.ts with 4 topics
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
12
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m297.56 KB[39m
|
|
12
|
+
[32mESM[39m ⚡️ Build success in 32ms
|
|
13
13
|
[34mDTS[39m Build start
|
|
14
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
14
|
+
[32mDTS[39m ⚡️ Build success in 4608ms
|
|
15
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m30.41 KB[39m
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
> @poncho-ai/harness@0.26.0 test /Users/cesar/Dev/latitude/poncho-ai/packages/harness
|
|
3
|
+
> vitest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[7m[1m[36m RUN [39m[22m[27m [36mv1.6.1[39m [90m/Users/cesar/Dev/latitude/poncho-ai/packages/harness[39m
|
|
7
|
+
|
|
8
|
+
[90mstdout[2m | test/mcp.test.ts[2m > [22m[2mmcp bridge protocol transports[2m > [22m[2mdiscovers and calls tools over streamable HTTP[22m[39m
|
|
9
|
+
[poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
|
|
10
|
+
[poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
|
|
11
|
+
|
|
12
|
+
[90mstdout[2m | test/mcp.test.ts[2m > [22m[2mmcp bridge protocol transports[2m > [22m[2msends custom headers alongside bearer token[22m[39m
|
|
13
|
+
[poncho][mcp] {"event":"catalog.loaded","server":"custom-headers","discoveredCount":1}
|
|
14
|
+
|
|
15
|
+
[90mstderr[2m | test/mcp.test.ts[2m > [22m[2mmcp bridge protocol transports[2m > [22m[2mskips discovery when bearer token env value is missing[22m[39m
|
|
16
|
+
[90mstdout[2m | test/mcp.test.ts[2m > [22m[2mmcp bridge protocol transports[2m > [22m[2mselects discovered tools by requested patterns[22m[39m
|
|
17
|
+
[poncho][mcp] {"event":"auth.token_missing","server":"remote","tokenEnv":"MISSING_TOKEN_ENV"}
|
|
18
|
+
[poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
|
|
19
|
+
[poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
|
|
20
|
+
|
|
21
|
+
[poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
|
|
22
|
+
|
|
23
|
+
[90mstdout[2m | test/mcp.test.ts[2m > [22m[2mmcp bridge protocol transports[2m > [22m[2mskips discovery when bearer token env value is missing[22m[39m
|
|
24
|
+
[poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":0,"filteredByPolicyCount":0,"filteredByIntentCount":0}
|
|
25
|
+
|
|
26
|
+
[event] step:completed {"type":"step:completed","step":1,"duration":1}
|
|
27
|
+
[32m✓[39m test/telemetry.test.ts [2m ([22m[2m3 tests[22m[2m)[22m[90m 5[2mms[22m[39m
|
|
28
|
+
[event] step:started {"type":"step:started","step":2}
|
|
29
|
+
[32m✓[39m test/schema-converter.test.ts [2m ([22m[2m27 tests[22m[2m)[22m[90m 13[2mms[22m[39m
|
|
30
|
+
[90mstdout[2m | test/mcp.test.ts[2m > [22m[2mmcp bridge protocol transports[2m > [22m[2mreturns actionable errors for 403 permission failures[22m[39m
|
|
31
|
+
[poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
|
|
32
|
+
[poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
|
|
33
|
+
|
|
34
|
+
[32m✓[39m test/mcp.test.ts [2m ([22m[2m7 tests[22m[2m)[22m[90m 84[2mms[22m[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.29.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#51](https://github.com/cesr/poncho-ai/pull/51) [`eb661a5`](https://github.com/cesr/poncho-ai/commit/eb661a554da6839702651671db8a8820ceb13f35) Thanks [@cesr](https://github.com/cesr)! - Add generic OTLP trace exporter for sending OpenTelemetry traces to any collector (Jaeger, Grafana Tempo, Honeycomb, etc.). Configure via `telemetry.otlp` as a URL string or `{ url, headers }` object. Works alongside or instead of Latitude telemetry.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`eb661a5`](https://github.com/cesr/poncho-ai/commit/eb661a554da6839702651671db8a8820ceb13f35)]:
|
|
12
|
+
- @poncho-ai/sdk@1.6.2
|
|
13
|
+
|
|
14
|
+
## 0.28.3
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [`87f844b`](https://github.com/cesr/poncho-ai/commit/87f844b0a76ece87e4bba78eaf73392f857cdef2) Thanks [@cesr](https://github.com/cesr)! - Fix tool execution blowing past serverless timeout and cross-skill script paths
|
|
19
|
+
- Race tool batch execution against remaining soft deadline so parallel tools can't push past the hard platform timeout
|
|
20
|
+
- Add post-tool-execution soft deadline checkpoint for tools that finish just past the deadline
|
|
21
|
+
- Allow skill scripts to reference sibling directories (e.g. ../scripts/current-date.ts)
|
|
22
|
+
- Catch script path normalization errors in approval check instead of crashing the run
|
|
23
|
+
|
|
3
24
|
## 0.28.2
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -176,6 +176,11 @@ interface Conversation {
|
|
|
176
176
|
/** Harness-internal message chain preserved across continuation runs.
|
|
177
177
|
* Cleared when a run completes without continuation. */
|
|
178
178
|
_continuationMessages?: Message[];
|
|
179
|
+
/** Full structured message chain from the last harness run, including
|
|
180
|
+
* tool-call and tool-result messages the model needs for context.
|
|
181
|
+
* Unlike `_continuationMessages`, this is always set after a run
|
|
182
|
+
* and does NOT signal that a continuation is pending. */
|
|
183
|
+
_harnessMessages?: Message[];
|
|
179
184
|
createdAt: number;
|
|
180
185
|
updatedAt: number;
|
|
181
186
|
}
|
|
@@ -411,7 +416,10 @@ interface PonchoConfig extends McpConfig {
|
|
|
411
416
|
};
|
|
412
417
|
telemetry?: {
|
|
413
418
|
enabled?: boolean;
|
|
414
|
-
otlp?: string
|
|
419
|
+
otlp?: string | {
|
|
420
|
+
url: string;
|
|
421
|
+
headers?: Record<string, string>;
|
|
422
|
+
};
|
|
415
423
|
latitude?: {
|
|
416
424
|
apiKeyEnv?: string;
|
|
417
425
|
projectIdEnv?: string;
|
|
@@ -623,6 +631,9 @@ declare class AgentHarness {
|
|
|
623
631
|
private readonly activeSkillNames;
|
|
624
632
|
private readonly registeredMcpToolNames;
|
|
625
633
|
private latitudeTelemetry?;
|
|
634
|
+
private otlpSpanProcessor?;
|
|
635
|
+
private otlpTracerProvider?;
|
|
636
|
+
private hasOtlpExporter;
|
|
626
637
|
private insideTelemetryCapture;
|
|
627
638
|
private _browserSession?;
|
|
628
639
|
private _browserMod?;
|
|
@@ -686,8 +697,9 @@ declare class AgentHarness {
|
|
|
686
697
|
shutdown(): Promise<void>;
|
|
687
698
|
listTools(): ToolDefinition[];
|
|
688
699
|
/**
|
|
689
|
-
* Wraps the run() generator with
|
|
690
|
-
*
|
|
700
|
+
* Wraps the run() generator with telemetry capture for complete trace coverage.
|
|
701
|
+
* Supports Latitude, generic OTLP, or both simultaneously.
|
|
702
|
+
* Streams events in real-time using an event queue pattern.
|
|
691
703
|
*/
|
|
692
704
|
runWithTelemetry(input: RunInput): AsyncGenerator<AgentEvent>;
|
|
693
705
|
compact(messages: Message[], options?: CompactMessagesOptions): Promise<CompactResult>;
|
|
@@ -804,9 +816,15 @@ declare const createSkillTools: (skills: SkillMetadata[], options?: {
|
|
|
804
816
|
}) => ToolDefinition[];
|
|
805
817
|
declare const normalizeScriptPolicyPath: (relativePath: string) => string;
|
|
806
818
|
|
|
819
|
+
interface OtlpConfig {
|
|
820
|
+
url: string;
|
|
821
|
+
headers?: Record<string, string>;
|
|
822
|
+
}
|
|
823
|
+
type OtlpOption = string | OtlpConfig;
|
|
824
|
+
declare function normalizeOtlp(opt: OtlpOption | undefined): OtlpConfig | undefined;
|
|
807
825
|
interface TelemetryConfig {
|
|
808
826
|
enabled?: boolean;
|
|
809
|
-
otlp?:
|
|
827
|
+
otlp?: OtlpOption;
|
|
810
828
|
latitude?: {
|
|
811
829
|
apiKeyEnv?: string;
|
|
812
830
|
projectIdEnv?: string;
|
|
@@ -824,4 +842,4 @@ declare class TelemetryEmitter {
|
|
|
824
842
|
|
|
825
843
|
declare const createSubagentTools: (manager: SubagentManager) => ToolDefinition[];
|
|
826
844
|
|
|
827
|
-
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentResult, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, compactMessages, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryTools, createModelProvider, createSearchTools, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, findSafeSplitPoint, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, ponchoDocsTool, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
|
845
|
+
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentResult, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, compactMessages, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryTools, createModelProvider, createSearchTools, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, findSafeSplitPoint, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeOtlp, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, ponchoDocsTool, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
package/dist/index.js
CHANGED
|
@@ -1529,11 +1529,17 @@ export default {
|
|
|
1529
1529
|
},
|
|
1530
1530
|
},
|
|
1531
1531
|
|
|
1532
|
-
// Telemetry destination
|
|
1532
|
+
// Telemetry destination \u2014 generic OTLP and/or Latitude
|
|
1533
1533
|
telemetry: {
|
|
1534
1534
|
enabled: true,
|
|
1535
|
+
// Generic OTLP: string shorthand or { url, headers? } object
|
|
1535
1536
|
otlp: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
1536
|
-
//
|
|
1537
|
+
// With auth headers (Honeycomb, Grafana Cloud, etc.):
|
|
1538
|
+
// otlp: {
|
|
1539
|
+
// url: 'https://api.honeycomb.io/v1/traces',
|
|
1540
|
+
// headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY },
|
|
1541
|
+
// },
|
|
1542
|
+
// Latitude (reads from LATITUDE_API_KEY and LATITUDE_PROJECT_ID env vars by default)
|
|
1537
1543
|
latitude: {
|
|
1538
1544
|
// apiKeyEnv: 'LATITUDE_API_KEY', // default
|
|
1539
1545
|
// projectIdEnv: 'LATITUDE_PROJECT_ID', // default
|
|
@@ -1606,7 +1612,7 @@ Remote storage keys are namespaced and versioned, for example \`poncho:v1:<agent
|
|
|
1606
1612
|
| \`PONCHO_AUTH_TOKEN\` | No | Unified auth token (Web UI passphrase + API Bearer token) |
|
|
1607
1613
|
| \`PONCHO_INTERNAL_SECRET\` | No | Shared secret used by internal serverless callbacks (recommended for Vercel/Lambda) |
|
|
1608
1614
|
| \`PONCHO_SELF_BASE_URL\` | No | Explicit base URL for internal self-callbacks when auto-detection is unavailable |
|
|
1609
|
-
| \`OTEL_EXPORTER_OTLP_ENDPOINT\` | No |
|
|
1615
|
+
| \`OTEL_EXPORTER_OTLP_ENDPOINT\` | No | OTLP trace endpoint (Jaeger, Tempo, Honeycomb, etc.) |
|
|
1610
1616
|
| \`LATITUDE_API_KEY\` | No | Latitude dashboard integration |
|
|
1611
1617
|
| \`LATITUDE_PROJECT_ID\` | No | Latitude project identifier for capture traces |
|
|
1612
1618
|
| \`LATITUDE_PATH\` | No | Latitude prompt path for grouping traces |
|
|
@@ -1641,23 +1647,45 @@ Logs print to console:
|
|
|
1641
1647
|
[event] run:completed {"type":"run:completed","runId":"run_abc123","result":{"status":"completed","response":"...","steps":3,"tokens":{"input":1500,"output":840}}}
|
|
1642
1648
|
\`\`\`
|
|
1643
1649
|
|
|
1644
|
-
### Production telemetry
|
|
1650
|
+
### Production telemetry (generic OTLP)
|
|
1645
1651
|
|
|
1646
|
-
Send
|
|
1652
|
+
Send full OpenTelemetry traces (agent runs, LLM calls, tool executions) to any
|
|
1653
|
+
OTLP-compatible collector \u2014 Jaeger, Grafana Tempo, Honeycomb, Datadog, etc.
|
|
1647
1654
|
|
|
1648
1655
|
\`\`\`bash
|
|
1649
|
-
#
|
|
1650
|
-
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
|
|
1656
|
+
# Simple: just a URL
|
|
1657
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com/v1/traces
|
|
1651
1658
|
\`\`\`
|
|
1652
1659
|
|
|
1653
|
-
|
|
1660
|
+
\`\`\`javascript
|
|
1661
|
+
// poncho.config.js \u2014 string shorthand
|
|
1662
|
+
export default {
|
|
1663
|
+
telemetry: {
|
|
1664
|
+
otlp: 'https://otel.example.com/v1/traces',
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
\`\`\`
|
|
1668
|
+
|
|
1669
|
+
\`\`\`javascript
|
|
1670
|
+
// poncho.config.js \u2014 with auth headers (Honeycomb, Grafana Cloud, etc.)
|
|
1671
|
+
export default {
|
|
1672
|
+
telemetry: {
|
|
1673
|
+
otlp: {
|
|
1674
|
+
url: 'https://api.honeycomb.io/v1/traces',
|
|
1675
|
+
headers: {
|
|
1676
|
+
'x-honeycomb-team': process.env.HONEYCOMB_API_KEY,
|
|
1677
|
+
},
|
|
1678
|
+
},
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
\`\`\`
|
|
1682
|
+
|
|
1683
|
+
You can also use a custom event handler for non-OTLP destinations:
|
|
1654
1684
|
|
|
1655
1685
|
\`\`\`javascript
|
|
1656
1686
|
// poncho.config.js
|
|
1657
1687
|
export default {
|
|
1658
1688
|
telemetry: {
|
|
1659
|
-
otlp: 'https://otel.example.com',
|
|
1660
|
-
// Or custom handler
|
|
1661
1689
|
handler: async (event) => {
|
|
1662
1690
|
await sendToMyLoggingService(event)
|
|
1663
1691
|
}
|
|
@@ -1687,6 +1715,8 @@ telemetry: {
|
|
|
1687
1715
|
}
|
|
1688
1716
|
\`\`\`
|
|
1689
1717
|
|
|
1718
|
+
Both \`otlp\` and \`latitude\` can be configured simultaneously \u2014 all spans flow to both destinations.
|
|
1719
|
+
|
|
1690
1720
|
## Security
|
|
1691
1721
|
|
|
1692
1722
|
### Protect your endpoint
|
|
@@ -4087,7 +4117,8 @@ var createSkillTools = (skills, options) => {
|
|
|
4087
4117
|
error: `Unknown skill: "${name}". Available skills: ${knownNames}`
|
|
4088
4118
|
};
|
|
4089
4119
|
}
|
|
4090
|
-
const
|
|
4120
|
+
const projectRoot = options?.workingDir ?? process.cwd();
|
|
4121
|
+
const resolved2 = resolveScriptPath(skill.skillDir, script, projectRoot);
|
|
4091
4122
|
if (options?.isScriptAllowed && !options.isScriptAllowed(name, resolved2.relativePath)) {
|
|
4092
4123
|
return {
|
|
4093
4124
|
error: `Script "${resolved2.relativePath}" for skill "${name}" is not allowed by policy.`
|
|
@@ -4175,7 +4206,7 @@ var collectScriptFiles = async (directory) => {
|
|
|
4175
4206
|
var normalizeScriptPolicyPath = (relativePath) => {
|
|
4176
4207
|
const trimmed = relativePath.trim();
|
|
4177
4208
|
const normalized = normalize2(trimmed).split(sep2).join("/");
|
|
4178
|
-
if (normalized.startsWith("
|
|
4209
|
+
if (normalized.startsWith("/")) {
|
|
4179
4210
|
throw new Error("Script path must be relative and within the allowed directory");
|
|
4180
4211
|
}
|
|
4181
4212
|
const withoutDotPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
@@ -4184,10 +4215,11 @@ var normalizeScriptPolicyPath = (relativePath) => {
|
|
|
4184
4215
|
}
|
|
4185
4216
|
return withoutDotPrefix;
|
|
4186
4217
|
};
|
|
4187
|
-
var resolveScriptPath = (baseDir, relativePath) => {
|
|
4218
|
+
var resolveScriptPath = (baseDir, relativePath, containmentDir) => {
|
|
4188
4219
|
const normalized = normalizeScriptPolicyPath(relativePath);
|
|
4189
4220
|
const fullPath = resolve9(baseDir, normalized);
|
|
4190
|
-
|
|
4221
|
+
const boundary = resolve9(containmentDir ?? baseDir);
|
|
4222
|
+
if (!fullPath.startsWith(`${boundary}${sep2}`) && fullPath !== boundary) {
|
|
4191
4223
|
throw new Error("Script path must stay inside the allowed directory");
|
|
4192
4224
|
}
|
|
4193
4225
|
const extension = extname(fullPath).toLowerCase();
|
|
@@ -4579,6 +4611,78 @@ var createSubagentTools = (manager) => [
|
|
|
4579
4611
|
|
|
4580
4612
|
// src/harness.ts
|
|
4581
4613
|
import { LatitudeTelemetry } from "@latitude-data/telemetry";
|
|
4614
|
+
import { trace, context as otelContext, SpanStatusCode } from "@opentelemetry/api";
|
|
4615
|
+
import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
|
4616
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
4617
|
+
|
|
4618
|
+
// src/telemetry.ts
|
|
4619
|
+
var MAX_FIELD_LENGTH = 200;
|
|
4620
|
+
function sanitizeEventForLog(event) {
|
|
4621
|
+
return JSON.stringify(event, (_key, value) => {
|
|
4622
|
+
if (typeof value === "string" && value.length > MAX_FIELD_LENGTH) {
|
|
4623
|
+
return `${value.slice(0, 80)}...[${value.length} chars]`;
|
|
4624
|
+
}
|
|
4625
|
+
return value;
|
|
4626
|
+
});
|
|
4627
|
+
}
|
|
4628
|
+
function normalizeOtlp(opt) {
|
|
4629
|
+
if (!opt) return void 0;
|
|
4630
|
+
if (typeof opt === "string") return opt ? { url: opt } : void 0;
|
|
4631
|
+
return opt.url ? opt : void 0;
|
|
4632
|
+
}
|
|
4633
|
+
var TelemetryEmitter = class {
|
|
4634
|
+
config;
|
|
4635
|
+
constructor(config) {
|
|
4636
|
+
this.config = config;
|
|
4637
|
+
}
|
|
4638
|
+
async emit(event) {
|
|
4639
|
+
if (this.config?.enabled === false) {
|
|
4640
|
+
return;
|
|
4641
|
+
}
|
|
4642
|
+
if (this.config?.handler) {
|
|
4643
|
+
await this.config.handler(event);
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
const otlp = normalizeOtlp(this.config?.otlp);
|
|
4647
|
+
if (otlp) {
|
|
4648
|
+
await this.sendOtlp(event, otlp);
|
|
4649
|
+
}
|
|
4650
|
+
process.stdout.write(`[event] ${event.type} ${sanitizeEventForLog(event)}
|
|
4651
|
+
`);
|
|
4652
|
+
}
|
|
4653
|
+
async sendOtlp(event, otlp) {
|
|
4654
|
+
try {
|
|
4655
|
+
await fetch(otlp.url, {
|
|
4656
|
+
method: "POST",
|
|
4657
|
+
headers: { "Content-Type": "application/json", ...otlp.headers },
|
|
4658
|
+
body: JSON.stringify({
|
|
4659
|
+
resourceLogs: [
|
|
4660
|
+
{
|
|
4661
|
+
scopeLogs: [
|
|
4662
|
+
{
|
|
4663
|
+
logRecords: [
|
|
4664
|
+
{
|
|
4665
|
+
timeUnixNano: String(Date.now() * 1e6),
|
|
4666
|
+
severityText: "INFO",
|
|
4667
|
+
body: { stringValue: event.type },
|
|
4668
|
+
attributes: [
|
|
4669
|
+
{
|
|
4670
|
+
key: "event.payload",
|
|
4671
|
+
value: { stringValue: JSON.stringify(event) }
|
|
4672
|
+
}
|
|
4673
|
+
]
|
|
4674
|
+
}
|
|
4675
|
+
]
|
|
4676
|
+
}
|
|
4677
|
+
]
|
|
4678
|
+
}
|
|
4679
|
+
]
|
|
4680
|
+
})
|
|
4681
|
+
});
|
|
4682
|
+
} catch {
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
};
|
|
4582
4686
|
|
|
4583
4687
|
// src/tool-dispatcher.ts
|
|
4584
4688
|
var ToolDispatcher = class {
|
|
@@ -5111,6 +5215,9 @@ var AgentHarness = class _AgentHarness {
|
|
|
5111
5215
|
activeSkillNames = /* @__PURE__ */ new Set();
|
|
5112
5216
|
registeredMcpToolNames = /* @__PURE__ */ new Set();
|
|
5113
5217
|
latitudeTelemetry;
|
|
5218
|
+
otlpSpanProcessor;
|
|
5219
|
+
otlpTracerProvider;
|
|
5220
|
+
hasOtlpExporter = false;
|
|
5114
5221
|
insideTelemetryCapture = false;
|
|
5115
5222
|
_browserSession;
|
|
5116
5223
|
_browserMod;
|
|
@@ -5325,10 +5432,15 @@ var AgentHarness = class _AgentHarness {
|
|
|
5325
5432
|
if (!rawScript) {
|
|
5326
5433
|
return false;
|
|
5327
5434
|
}
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5435
|
+
let canonicalPath;
|
|
5436
|
+
try {
|
|
5437
|
+
canonicalPath = normalizeRelativeScriptPattern(
|
|
5438
|
+
`./${normalizeScriptPolicyPath(rawScript)}`,
|
|
5439
|
+
"run_skill_script input.script"
|
|
5440
|
+
);
|
|
5441
|
+
} catch {
|
|
5442
|
+
return true;
|
|
5443
|
+
}
|
|
5332
5444
|
const scriptPatterns = this.getRequestedScriptApprovalPatterns();
|
|
5333
5445
|
return scriptPatterns.some(
|
|
5334
5446
|
(pattern) => matchesRelativeScriptPattern(canonicalPath, pattern)
|
|
@@ -5553,6 +5665,31 @@ var AgentHarness = class _AgentHarness {
|
|
|
5553
5665
|
`[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}. Traces will NOT be sent.`
|
|
5554
5666
|
);
|
|
5555
5667
|
}
|
|
5668
|
+
const otlpConfig = telemetryEnabled ? normalizeOtlp(config?.telemetry?.otlp) : void 0;
|
|
5669
|
+
if (otlpConfig) {
|
|
5670
|
+
const exporter = new OTLPTraceExporter({
|
|
5671
|
+
url: otlpConfig.url,
|
|
5672
|
+
headers: otlpConfig.headers
|
|
5673
|
+
});
|
|
5674
|
+
const processor = new BatchSpanProcessor(exporter);
|
|
5675
|
+
this.otlpSpanProcessor = processor;
|
|
5676
|
+
if (this.latitudeTelemetry) {
|
|
5677
|
+
const globalProvider = trace.getTracerProvider();
|
|
5678
|
+
const delegate = globalProvider.getDelegate?.() ?? globalProvider;
|
|
5679
|
+
if (typeof delegate.addSpanProcessor === "function") {
|
|
5680
|
+
delegate.addSpanProcessor(processor);
|
|
5681
|
+
}
|
|
5682
|
+
console.info(`[poncho][telemetry] OTLP exporter added (piggybacking on Latitude provider) \u2192 ${otlpConfig.url}`);
|
|
5683
|
+
} else {
|
|
5684
|
+
const provider2 = new NodeTracerProvider({
|
|
5685
|
+
spanProcessors: [processor]
|
|
5686
|
+
});
|
|
5687
|
+
provider2.register();
|
|
5688
|
+
this.otlpTracerProvider = provider2;
|
|
5689
|
+
console.info(`[poncho][telemetry] OTLP exporter active (standalone provider) \u2192 ${otlpConfig.url}`);
|
|
5690
|
+
}
|
|
5691
|
+
this.hasOtlpExporter = true;
|
|
5692
|
+
}
|
|
5556
5693
|
}
|
|
5557
5694
|
async buildBrowserStoragePersistence(config, sessionId) {
|
|
5558
5695
|
const provider = config.storage?.provider ?? config.state?.provider ?? "local";
|
|
@@ -5703,13 +5840,31 @@ var AgentHarness = class _AgentHarness {
|
|
|
5703
5840
|
});
|
|
5704
5841
|
this.latitudeTelemetry = void 0;
|
|
5705
5842
|
}
|
|
5843
|
+
if (this.otlpSpanProcessor) {
|
|
5844
|
+
await this.otlpSpanProcessor.shutdown().catch((err) => {
|
|
5845
|
+
console.warn(
|
|
5846
|
+
`[poncho][telemetry] OTLP span processor shutdown error: ${err instanceof Error ? err.message : String(err)}`
|
|
5847
|
+
);
|
|
5848
|
+
});
|
|
5849
|
+
this.otlpSpanProcessor = void 0;
|
|
5850
|
+
}
|
|
5851
|
+
if (this.otlpTracerProvider) {
|
|
5852
|
+
await this.otlpTracerProvider.shutdown().catch((err) => {
|
|
5853
|
+
console.warn(
|
|
5854
|
+
`[poncho][telemetry] OTLP tracer provider shutdown error: ${err instanceof Error ? err.message : String(err)}`
|
|
5855
|
+
);
|
|
5856
|
+
});
|
|
5857
|
+
this.otlpTracerProvider = void 0;
|
|
5858
|
+
}
|
|
5859
|
+
this.hasOtlpExporter = false;
|
|
5706
5860
|
}
|
|
5707
5861
|
listTools() {
|
|
5708
5862
|
return this.dispatcher.list();
|
|
5709
5863
|
}
|
|
5710
5864
|
/**
|
|
5711
|
-
* Wraps the run() generator with
|
|
5712
|
-
*
|
|
5865
|
+
* Wraps the run() generator with telemetry capture for complete trace coverage.
|
|
5866
|
+
* Supports Latitude, generic OTLP, or both simultaneously.
|
|
5867
|
+
* Streams events in real-time using an event queue pattern.
|
|
5713
5868
|
*/
|
|
5714
5869
|
async *runWithTelemetry(input) {
|
|
5715
5870
|
const config = this.loadedConfig;
|
|
@@ -5776,6 +5931,39 @@ var AgentHarness = class _AgentHarness {
|
|
|
5776
5931
|
}
|
|
5777
5932
|
}
|
|
5778
5933
|
}
|
|
5934
|
+
} else if (this.hasOtlpExporter) {
|
|
5935
|
+
const tracer = trace.getTracer("poncho");
|
|
5936
|
+
const agentName = this.parsedAgent?.frontmatter.name ?? "agent";
|
|
5937
|
+
const rootSpan = tracer.startSpan(`agent.run ${agentName}`);
|
|
5938
|
+
rootSpan.setAttribute("poncho.agent.name", agentName);
|
|
5939
|
+
if (input.conversationId) {
|
|
5940
|
+
rootSpan.setAttribute("poncho.conversation.id", input.conversationId);
|
|
5941
|
+
}
|
|
5942
|
+
const spanContext = trace.setSpan(otelContext.active(), rootSpan);
|
|
5943
|
+
this.insideTelemetryCapture = true;
|
|
5944
|
+
try {
|
|
5945
|
+
const gen = this.run(input);
|
|
5946
|
+
let next;
|
|
5947
|
+
do {
|
|
5948
|
+
next = await otelContext.with(spanContext, () => gen.next());
|
|
5949
|
+
if (!next.done) yield next.value;
|
|
5950
|
+
} while (!next.done);
|
|
5951
|
+
rootSpan.setStatus({ code: SpanStatusCode.OK });
|
|
5952
|
+
} catch (error) {
|
|
5953
|
+
rootSpan.setStatus({
|
|
5954
|
+
code: SpanStatusCode.ERROR,
|
|
5955
|
+
message: error instanceof Error ? error.message : String(error)
|
|
5956
|
+
});
|
|
5957
|
+
rootSpan.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
5958
|
+
throw error;
|
|
5959
|
+
} finally {
|
|
5960
|
+
this.insideTelemetryCapture = false;
|
|
5961
|
+
rootSpan.end();
|
|
5962
|
+
try {
|
|
5963
|
+
await this.otlpSpanProcessor?.forceFlush();
|
|
5964
|
+
} catch {
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5779
5967
|
} else {
|
|
5780
5968
|
yield* this.run(input);
|
|
5781
5969
|
}
|
|
@@ -6264,7 +6452,7 @@ ${textContent}` };
|
|
|
6264
6452
|
abortSignal: input.abortSignal,
|
|
6265
6453
|
...typeof maxTokens === "number" ? { maxTokens } : {},
|
|
6266
6454
|
experimental_telemetry: {
|
|
6267
|
-
isEnabled: telemetryEnabled && !!this.latitudeTelemetry,
|
|
6455
|
+
isEnabled: telemetryEnabled && !!(this.latitudeTelemetry || this.hasOtlpExporter),
|
|
6268
6456
|
recordInputs: true,
|
|
6269
6457
|
recordOutputs: true
|
|
6270
6458
|
}
|
|
@@ -6435,6 +6623,13 @@ ${textContent}` };
|
|
|
6435
6623
|
`[poncho][harness] Model "${modelName}" returned an empty response with finishReason="stop" on step ${step}.`
|
|
6436
6624
|
);
|
|
6437
6625
|
}
|
|
6626
|
+
if (fullText.length > 0) {
|
|
6627
|
+
messages.push({
|
|
6628
|
+
role: "assistant",
|
|
6629
|
+
content: fullText,
|
|
6630
|
+
metadata: { timestamp: now(), id: randomUUID3(), step }
|
|
6631
|
+
});
|
|
6632
|
+
}
|
|
6438
6633
|
responseText = fullText;
|
|
6439
6634
|
yield pushEvent({
|
|
6440
6635
|
type: "step:completed",
|
|
@@ -6452,7 +6647,8 @@ ${textContent}` };
|
|
|
6452
6647
|
},
|
|
6453
6648
|
duration: now() - start,
|
|
6454
6649
|
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
6455
|
-
contextWindow
|
|
6650
|
+
contextWindow,
|
|
6651
|
+
continuationMessages: [...messages]
|
|
6456
6652
|
};
|
|
6457
6653
|
yield pushEvent({ type: "run:completed", runId, result: result2 });
|
|
6458
6654
|
return;
|
|
@@ -6548,8 +6744,66 @@ ${textContent}` };
|
|
|
6548
6744
|
})
|
|
6549
6745
|
);
|
|
6550
6746
|
}
|
|
6747
|
+
} else if (this.insideTelemetryCapture && this.hasOtlpExporter) {
|
|
6748
|
+
const tracer = trace.getTracer("poncho");
|
|
6749
|
+
for (const call of approvedCalls) {
|
|
6750
|
+
const span = tracer.startSpan(`tool ${call.name}`, {
|
|
6751
|
+
attributes: {
|
|
6752
|
+
"poncho.tool.name": call.name,
|
|
6753
|
+
"poncho.tool.call_id": call.id,
|
|
6754
|
+
"poncho.tool.arguments": JSON.stringify(call.input)
|
|
6755
|
+
}
|
|
6756
|
+
});
|
|
6757
|
+
toolSpans.set(call.id, {
|
|
6758
|
+
end(opts) {
|
|
6759
|
+
if (opts.result.isError) {
|
|
6760
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(opts.result.value) });
|
|
6761
|
+
} else {
|
|
6762
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
6763
|
+
}
|
|
6764
|
+
span.end();
|
|
6765
|
+
}
|
|
6766
|
+
});
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6769
|
+
const TOOL_DEADLINE_SENTINEL = /* @__PURE__ */ Symbol("tool_deadline");
|
|
6770
|
+
const toolDeadlineRemainingMs = softDeadlineMs > 0 ? softDeadlineMs - (now() - start) : Infinity;
|
|
6771
|
+
let batchResults;
|
|
6772
|
+
if (approvedCalls.length === 0) {
|
|
6773
|
+
batchResults = [];
|
|
6774
|
+
} else if (toolDeadlineRemainingMs <= 0) {
|
|
6775
|
+
batchResults = TOOL_DEADLINE_SENTINEL;
|
|
6776
|
+
} else if (toolDeadlineRemainingMs < Infinity) {
|
|
6777
|
+
const raced = await Promise.race([
|
|
6778
|
+
this.dispatcher.executeBatch(approvedCalls, toolContext),
|
|
6779
|
+
new Promise(
|
|
6780
|
+
(resolve12) => setTimeout(() => resolve12(TOOL_DEADLINE_SENTINEL), toolDeadlineRemainingMs)
|
|
6781
|
+
)
|
|
6782
|
+
]);
|
|
6783
|
+
if (raced === TOOL_DEADLINE_SENTINEL) {
|
|
6784
|
+
batchResults = TOOL_DEADLINE_SENTINEL;
|
|
6785
|
+
} else {
|
|
6786
|
+
batchResults = raced;
|
|
6787
|
+
}
|
|
6788
|
+
} else {
|
|
6789
|
+
batchResults = await this.dispatcher.executeBatch(approvedCalls, toolContext);
|
|
6790
|
+
}
|
|
6791
|
+
if (batchResults === TOOL_DEADLINE_SENTINEL) {
|
|
6792
|
+
const result_ = {
|
|
6793
|
+
status: "completed",
|
|
6794
|
+
response: responseText + fullText,
|
|
6795
|
+
steps: step,
|
|
6796
|
+
tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
|
|
6797
|
+
duration: now() - start,
|
|
6798
|
+
continuation: true,
|
|
6799
|
+
continuationMessages: [...messages],
|
|
6800
|
+
maxSteps,
|
|
6801
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
6802
|
+
contextWindow
|
|
6803
|
+
};
|
|
6804
|
+
yield pushEvent({ type: "run:completed", runId, result: result_ });
|
|
6805
|
+
return;
|
|
6551
6806
|
}
|
|
6552
|
-
const batchResults = approvedCalls.length > 0 ? await this.dispatcher.executeBatch(approvedCalls, toolContext) : [];
|
|
6553
6807
|
if (isCancelled()) {
|
|
6554
6808
|
yield emitCancellation();
|
|
6555
6809
|
return;
|
|
@@ -6637,6 +6891,22 @@ ${textContent}` };
|
|
|
6637
6891
|
content: JSON.stringify(toolResultsForModel),
|
|
6638
6892
|
metadata: toolMsgMeta
|
|
6639
6893
|
});
|
|
6894
|
+
if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
|
|
6895
|
+
const result_ = {
|
|
6896
|
+
status: "completed",
|
|
6897
|
+
response: responseText + fullText,
|
|
6898
|
+
steps: step,
|
|
6899
|
+
tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
|
|
6900
|
+
duration: now() - start,
|
|
6901
|
+
continuation: true,
|
|
6902
|
+
continuationMessages: [...messages],
|
|
6903
|
+
maxSteps,
|
|
6904
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
6905
|
+
contextWindow
|
|
6906
|
+
};
|
|
6907
|
+
yield pushEvent({ type: "run:completed", runId, result: result_ });
|
|
6908
|
+
return;
|
|
6909
|
+
}
|
|
6640
6910
|
if (this.environment === "development") {
|
|
6641
6911
|
const agentChanged = await this.refreshAgentIfChanged();
|
|
6642
6912
|
const skillsChanged = await this.refreshSkillsIfChanged(true);
|
|
@@ -8006,73 +8276,6 @@ var createConversationStore = (config, options) => {
|
|
|
8006
8276
|
return new InMemoryConversationStore(ttl);
|
|
8007
8277
|
};
|
|
8008
8278
|
|
|
8009
|
-
// src/telemetry.ts
|
|
8010
|
-
var MAX_FIELD_LENGTH = 200;
|
|
8011
|
-
function sanitizeEventForLog(event) {
|
|
8012
|
-
return JSON.stringify(event, (_key, value) => {
|
|
8013
|
-
if (typeof value === "string" && value.length > MAX_FIELD_LENGTH) {
|
|
8014
|
-
return `${value.slice(0, 80)}...[${value.length} chars]`;
|
|
8015
|
-
}
|
|
8016
|
-
return value;
|
|
8017
|
-
});
|
|
8018
|
-
}
|
|
8019
|
-
var TelemetryEmitter = class {
|
|
8020
|
-
config;
|
|
8021
|
-
constructor(config) {
|
|
8022
|
-
this.config = config;
|
|
8023
|
-
}
|
|
8024
|
-
async emit(event) {
|
|
8025
|
-
if (this.config?.enabled === false) {
|
|
8026
|
-
return;
|
|
8027
|
-
}
|
|
8028
|
-
if (this.config?.handler) {
|
|
8029
|
-
await this.config.handler(event);
|
|
8030
|
-
return;
|
|
8031
|
-
}
|
|
8032
|
-
if (this.config?.otlp) {
|
|
8033
|
-
await this.sendOtlp(event);
|
|
8034
|
-
}
|
|
8035
|
-
process.stdout.write(`[event] ${event.type} ${sanitizeEventForLog(event)}
|
|
8036
|
-
`);
|
|
8037
|
-
}
|
|
8038
|
-
async sendOtlp(event) {
|
|
8039
|
-
const endpoint = this.config?.otlp;
|
|
8040
|
-
if (!endpoint) {
|
|
8041
|
-
return;
|
|
8042
|
-
}
|
|
8043
|
-
try {
|
|
8044
|
-
await fetch(endpoint, {
|
|
8045
|
-
method: "POST",
|
|
8046
|
-
headers: { "Content-Type": "application/json" },
|
|
8047
|
-
body: JSON.stringify({
|
|
8048
|
-
resourceLogs: [
|
|
8049
|
-
{
|
|
8050
|
-
scopeLogs: [
|
|
8051
|
-
{
|
|
8052
|
-
logRecords: [
|
|
8053
|
-
{
|
|
8054
|
-
timeUnixNano: String(Date.now() * 1e6),
|
|
8055
|
-
severityText: "INFO",
|
|
8056
|
-
body: { stringValue: event.type },
|
|
8057
|
-
attributes: [
|
|
8058
|
-
{
|
|
8059
|
-
key: "event.payload",
|
|
8060
|
-
value: { stringValue: JSON.stringify(event) }
|
|
8061
|
-
}
|
|
8062
|
-
]
|
|
8063
|
-
}
|
|
8064
|
-
]
|
|
8065
|
-
}
|
|
8066
|
-
]
|
|
8067
|
-
}
|
|
8068
|
-
]
|
|
8069
|
-
})
|
|
8070
|
-
});
|
|
8071
|
-
} catch {
|
|
8072
|
-
}
|
|
8073
|
-
}
|
|
8074
|
-
};
|
|
8075
|
-
|
|
8076
8279
|
// src/index.ts
|
|
8077
8280
|
import { defineTool as defineTool7 } from "@poncho-ai/sdk";
|
|
8078
8281
|
export {
|
|
@@ -8120,6 +8323,7 @@ export {
|
|
|
8120
8323
|
loadSkillContext,
|
|
8121
8324
|
loadSkillInstructions,
|
|
8122
8325
|
loadSkillMetadata,
|
|
8326
|
+
normalizeOtlp,
|
|
8123
8327
|
normalizeScriptPolicyPath,
|
|
8124
8328
|
parseAgentFile,
|
|
8125
8329
|
parseAgentMarkdown,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"@aws-sdk/client-dynamodb": "^3.988.0",
|
|
26
26
|
"@latitude-data/telemetry": "^2.0.4",
|
|
27
27
|
"@opentelemetry/api": "1.9.0",
|
|
28
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
|
|
29
|
+
"@opentelemetry/sdk-trace-node": "^2.6.0",
|
|
28
30
|
"ai": "^6.0.86",
|
|
29
31
|
"cheerio": "^1.2.0",
|
|
30
32
|
"jiti": "^2.6.1",
|
|
@@ -32,7 +34,7 @@
|
|
|
32
34
|
"redis": "^5.10.0",
|
|
33
35
|
"yaml": "^2.4.0",
|
|
34
36
|
"zod": "^3.22.0",
|
|
35
|
-
"@poncho-ai/sdk": "1.6.
|
|
37
|
+
"@poncho-ai/sdk": "1.6.2"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"@types/mustache": "^4.2.6",
|
package/src/config.ts
CHANGED
|
@@ -104,7 +104,10 @@ export interface PonchoConfig extends McpConfig {
|
|
|
104
104
|
};
|
|
105
105
|
telemetry?: {
|
|
106
106
|
enabled?: boolean;
|
|
107
|
-
otlp?: string
|
|
107
|
+
otlp?: string | {
|
|
108
|
+
url: string;
|
|
109
|
+
headers?: Record<string, string>;
|
|
110
|
+
};
|
|
108
111
|
latitude?: {
|
|
109
112
|
apiKeyEnv?: string;
|
|
110
113
|
projectIdEnv?: string;
|
package/src/harness.ts
CHANGED
|
@@ -36,6 +36,10 @@ import { createSearchTools } from "./search-tools.js";
|
|
|
36
36
|
import { createSubagentTools } from "./subagent-tools.js";
|
|
37
37
|
import type { SubagentManager } from "./subagent-manager.js";
|
|
38
38
|
import { LatitudeTelemetry } from "@latitude-data/telemetry";
|
|
39
|
+
import { trace, context as otelContext, SpanStatusCode } from "@opentelemetry/api";
|
|
40
|
+
import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
|
41
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
42
|
+
import { normalizeOtlp } from "./telemetry.js";
|
|
39
43
|
import {
|
|
40
44
|
isSiblingScriptsPattern,
|
|
41
45
|
matchesRelativeScriptPattern,
|
|
@@ -560,6 +564,9 @@ export class AgentHarness {
|
|
|
560
564
|
private readonly activeSkillNames = new Set<string>();
|
|
561
565
|
private readonly registeredMcpToolNames = new Set<string>();
|
|
562
566
|
private latitudeTelemetry?: LatitudeTelemetry;
|
|
567
|
+
private otlpSpanProcessor?: BatchSpanProcessor;
|
|
568
|
+
private otlpTracerProvider?: NodeTracerProvider;
|
|
569
|
+
private hasOtlpExporter = false;
|
|
563
570
|
private insideTelemetryCapture = false;
|
|
564
571
|
private _browserSession?: unknown;
|
|
565
572
|
private _browserMod?: {
|
|
@@ -812,10 +819,15 @@ export class AgentHarness {
|
|
|
812
819
|
if (!rawScript) {
|
|
813
820
|
return false;
|
|
814
821
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
822
|
+
let canonicalPath: string;
|
|
823
|
+
try {
|
|
824
|
+
canonicalPath = normalizeRelativeScriptPattern(
|
|
825
|
+
`./${normalizeScriptPolicyPath(rawScript)}`,
|
|
826
|
+
"run_skill_script input.script",
|
|
827
|
+
);
|
|
828
|
+
} catch {
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
819
831
|
const scriptPatterns = this.getRequestedScriptApprovalPatterns();
|
|
820
832
|
return scriptPatterns.some((pattern) =>
|
|
821
833
|
matchesRelativeScriptPattern(canonicalPath, pattern),
|
|
@@ -1074,6 +1086,37 @@ export class AgentHarness {
|
|
|
1074
1086
|
`[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}. Traces will NOT be sent.`,
|
|
1075
1087
|
);
|
|
1076
1088
|
}
|
|
1089
|
+
|
|
1090
|
+
// Generic OTLP trace exporter — works alongside or instead of Latitude.
|
|
1091
|
+
const otlpConfig = telemetryEnabled ? normalizeOtlp(config?.telemetry?.otlp) : undefined;
|
|
1092
|
+
if (otlpConfig) {
|
|
1093
|
+
const exporter = new OTLPTraceExporter({
|
|
1094
|
+
url: otlpConfig.url,
|
|
1095
|
+
headers: otlpConfig.headers,
|
|
1096
|
+
});
|
|
1097
|
+
const processor = new BatchSpanProcessor(exporter);
|
|
1098
|
+
this.otlpSpanProcessor = processor;
|
|
1099
|
+
|
|
1100
|
+
if (this.latitudeTelemetry) {
|
|
1101
|
+
// Latitude already registered a global TracerProvider (v1.x) — add our
|
|
1102
|
+
// processor to it so every span flows to both destinations.
|
|
1103
|
+
const globalProvider = trace.getTracerProvider();
|
|
1104
|
+
const delegate = (globalProvider as unknown as { getDelegate?: () => unknown })
|
|
1105
|
+
.getDelegate?.() ?? globalProvider;
|
|
1106
|
+
if (typeof (delegate as Record<string, unknown>).addSpanProcessor === "function") {
|
|
1107
|
+
(delegate as unknown as { addSpanProcessor(p: BatchSpanProcessor): void }).addSpanProcessor(processor);
|
|
1108
|
+
}
|
|
1109
|
+
console.info(`[poncho][telemetry] OTLP exporter added (piggybacking on Latitude provider) → ${otlpConfig.url}`);
|
|
1110
|
+
} else {
|
|
1111
|
+
const provider = new NodeTracerProvider({
|
|
1112
|
+
spanProcessors: [processor],
|
|
1113
|
+
});
|
|
1114
|
+
provider.register();
|
|
1115
|
+
this.otlpTracerProvider = provider;
|
|
1116
|
+
console.info(`[poncho][telemetry] OTLP exporter active (standalone provider) → ${otlpConfig.url}`);
|
|
1117
|
+
}
|
|
1118
|
+
this.hasOtlpExporter = true;
|
|
1119
|
+
}
|
|
1077
1120
|
}
|
|
1078
1121
|
|
|
1079
1122
|
private async buildBrowserStoragePersistence(
|
|
@@ -1245,6 +1288,27 @@ export class AgentHarness {
|
|
|
1245
1288
|
});
|
|
1246
1289
|
this.latitudeTelemetry = undefined;
|
|
1247
1290
|
}
|
|
1291
|
+
if (this.otlpSpanProcessor) {
|
|
1292
|
+
await this.otlpSpanProcessor.shutdown().catch((err) => {
|
|
1293
|
+
console.warn(
|
|
1294
|
+
`[poncho][telemetry] OTLP span processor shutdown error: ${
|
|
1295
|
+
err instanceof Error ? err.message : String(err)
|
|
1296
|
+
}`,
|
|
1297
|
+
);
|
|
1298
|
+
});
|
|
1299
|
+
this.otlpSpanProcessor = undefined;
|
|
1300
|
+
}
|
|
1301
|
+
if (this.otlpTracerProvider) {
|
|
1302
|
+
await this.otlpTracerProvider.shutdown().catch((err) => {
|
|
1303
|
+
console.warn(
|
|
1304
|
+
`[poncho][telemetry] OTLP tracer provider shutdown error: ${
|
|
1305
|
+
err instanceof Error ? err.message : String(err)
|
|
1306
|
+
}`,
|
|
1307
|
+
);
|
|
1308
|
+
});
|
|
1309
|
+
this.otlpTracerProvider = undefined;
|
|
1310
|
+
}
|
|
1311
|
+
this.hasOtlpExporter = false;
|
|
1248
1312
|
}
|
|
1249
1313
|
|
|
1250
1314
|
listTools(): ToolDefinition[] {
|
|
@@ -1252,18 +1316,20 @@ export class AgentHarness {
|
|
|
1252
1316
|
}
|
|
1253
1317
|
|
|
1254
1318
|
/**
|
|
1255
|
-
* Wraps the run() generator with
|
|
1256
|
-
*
|
|
1319
|
+
* Wraps the run() generator with telemetry capture for complete trace coverage.
|
|
1320
|
+
* Supports Latitude, generic OTLP, or both simultaneously.
|
|
1321
|
+
* Streams events in real-time using an event queue pattern.
|
|
1257
1322
|
*/
|
|
1258
1323
|
async *runWithTelemetry(input: RunInput): AsyncGenerator<AgentEvent> {
|
|
1259
1324
|
const config = this.loadedConfig;
|
|
1260
1325
|
const telemetry = this.latitudeTelemetry;
|
|
1261
1326
|
|
|
1262
1327
|
if (telemetry) {
|
|
1328
|
+
// Latitude capture path — wraps run() inside telemetry.capture().
|
|
1329
|
+
// If OTLP is also configured, spans flow to both via the shared provider.
|
|
1263
1330
|
const latProjectIdEnv2 = config?.telemetry?.latitude?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
|
|
1264
1331
|
const projectId = parseInt(process.env[latProjectIdEnv2] ?? "", 10) as number;
|
|
1265
1332
|
const rawPath = config?.telemetry?.latitude?.path ?? this.parsedAgent?.frontmatter.name ?? 'agent';
|
|
1266
|
-
// Sanitize path for Latitude's DOCUMENT_PATH_REGEXP: /^([\w-]+\/)*([\w-.])+$/
|
|
1267
1333
|
const path = rawPath.replace(/[^\w\-./]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'agent';
|
|
1268
1334
|
|
|
1269
1335
|
const rawConversationId = input.conversationId ?? (
|
|
@@ -1271,7 +1337,6 @@ export class AgentHarness {
|
|
|
1271
1337
|
? input.parameters.__activeConversationId
|
|
1272
1338
|
: undefined
|
|
1273
1339
|
);
|
|
1274
|
-
// Latitude expects a UUID v4 for documentLogUuid; only pass it if valid
|
|
1275
1340
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1276
1341
|
const conversationUuid = rawConversationId && UUID_RE.test(rawConversationId)
|
|
1277
1342
|
? rawConversationId
|
|
@@ -1281,13 +1346,11 @@ export class AgentHarness {
|
|
|
1281
1346
|
`[poncho][telemetry] Latitude telemetry active – projectId=${projectId}, path="${path}"${conversationUuid ? `, conversation="${conversationUuid}"` : ""}`,
|
|
1282
1347
|
);
|
|
1283
1348
|
|
|
1284
|
-
// Event queue for streaming events in real-time
|
|
1285
1349
|
const eventQueue: AgentEvent[] = [];
|
|
1286
1350
|
let queueResolve: ((value: void) => void) | null = null;
|
|
1287
1351
|
let generatorDone = false;
|
|
1288
1352
|
let generatorError: Error | null = null;
|
|
1289
1353
|
|
|
1290
|
-
// Start the generator inside telemetry.capture() (runs in background)
|
|
1291
1354
|
const capturePromise = telemetry.capture({ projectId, path, conversationUuid }, async () => {
|
|
1292
1355
|
this.insideTelemetryCapture = true;
|
|
1293
1356
|
try {
|
|
@@ -1311,13 +1374,11 @@ export class AgentHarness {
|
|
|
1311
1374
|
}
|
|
1312
1375
|
});
|
|
1313
1376
|
|
|
1314
|
-
// Yield events from the queue as they arrive
|
|
1315
1377
|
try {
|
|
1316
1378
|
while (!generatorDone || eventQueue.length > 0) {
|
|
1317
1379
|
if (eventQueue.length > 0) {
|
|
1318
1380
|
yield eventQueue.shift()!;
|
|
1319
1381
|
} else if (!generatorDone) {
|
|
1320
|
-
// Wait for next event
|
|
1321
1382
|
await new Promise<void>((resolve) => {
|
|
1322
1383
|
queueResolve = resolve;
|
|
1323
1384
|
});
|
|
@@ -1339,8 +1400,47 @@ export class AgentHarness {
|
|
|
1339
1400
|
}
|
|
1340
1401
|
}
|
|
1341
1402
|
}
|
|
1403
|
+
} else if (this.hasOtlpExporter) {
|
|
1404
|
+
// Standalone OTLP path — create a root span for the agent run so all
|
|
1405
|
+
// child spans (LLM calls via Vercel AI SDK, tool spans) are grouped
|
|
1406
|
+
// under a single trace.
|
|
1407
|
+
const tracer = trace.getTracer("poncho");
|
|
1408
|
+
const agentName = this.parsedAgent?.frontmatter.name ?? "agent";
|
|
1409
|
+
|
|
1410
|
+
const rootSpan = tracer.startSpan(`agent.run ${agentName}`);
|
|
1411
|
+
rootSpan.setAttribute("poncho.agent.name", agentName);
|
|
1412
|
+
if (input.conversationId) {
|
|
1413
|
+
rootSpan.setAttribute("poncho.conversation.id", input.conversationId);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Bind the root span's context so every async step (including
|
|
1417
|
+
// streamText and tool calls) sees it as the parent span.
|
|
1418
|
+
const spanContext = trace.setSpan(otelContext.active(), rootSpan);
|
|
1419
|
+
this.insideTelemetryCapture = true;
|
|
1420
|
+
|
|
1421
|
+
try {
|
|
1422
|
+
const gen = this.run(input);
|
|
1423
|
+
let next: IteratorResult<AgentEvent>;
|
|
1424
|
+
do {
|
|
1425
|
+
next = await otelContext.with(spanContext, () => gen.next());
|
|
1426
|
+
if (!next.done) yield next.value;
|
|
1427
|
+
} while (!next.done);
|
|
1428
|
+
rootSpan.setStatus({ code: SpanStatusCode.OK });
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
rootSpan.setStatus({
|
|
1431
|
+
code: SpanStatusCode.ERROR,
|
|
1432
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1433
|
+
});
|
|
1434
|
+
rootSpan.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
1435
|
+
throw error;
|
|
1436
|
+
} finally {
|
|
1437
|
+
this.insideTelemetryCapture = false;
|
|
1438
|
+
rootSpan.end();
|
|
1439
|
+
try {
|
|
1440
|
+
await this.otlpSpanProcessor?.forceFlush();
|
|
1441
|
+
} catch { /* best-effort */ }
|
|
1442
|
+
}
|
|
1342
1443
|
} else {
|
|
1343
|
-
// No telemetry configured, just pass through
|
|
1344
1444
|
yield* this.run(input);
|
|
1345
1445
|
}
|
|
1346
1446
|
}
|
|
@@ -1936,7 +2036,7 @@ ${boundedMainMemory.trim()}`
|
|
|
1936
2036
|
abortSignal: input.abortSignal,
|
|
1937
2037
|
...(typeof maxTokens === "number" ? { maxTokens } : {}),
|
|
1938
2038
|
experimental_telemetry: {
|
|
1939
|
-
isEnabled: telemetryEnabled && !!this.latitudeTelemetry,
|
|
2039
|
+
isEnabled: telemetryEnabled && !!(this.latitudeTelemetry || this.hasOtlpExporter),
|
|
1940
2040
|
recordInputs: true,
|
|
1941
2041
|
recordOutputs: true,
|
|
1942
2042
|
},
|
|
@@ -2133,6 +2233,13 @@ ${boundedMainMemory.trim()}`
|
|
|
2133
2233
|
`[poncho][harness] Model "${modelName}" returned an empty response with finishReason="stop" on step ${step}.`,
|
|
2134
2234
|
);
|
|
2135
2235
|
}
|
|
2236
|
+
if (fullText.length > 0) {
|
|
2237
|
+
messages.push({
|
|
2238
|
+
role: "assistant",
|
|
2239
|
+
content: fullText,
|
|
2240
|
+
metadata: { timestamp: now(), id: randomUUID(), step },
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2136
2243
|
responseText = fullText;
|
|
2137
2244
|
yield pushEvent({
|
|
2138
2245
|
type: "step:completed",
|
|
@@ -2151,6 +2258,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2151
2258
|
duration: now() - start,
|
|
2152
2259
|
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
2153
2260
|
contextWindow,
|
|
2261
|
+
continuationMessages: [...messages],
|
|
2154
2262
|
};
|
|
2155
2263
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
2156
2264
|
return;
|
|
@@ -2270,7 +2378,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2270
2378
|
return;
|
|
2271
2379
|
}
|
|
2272
2380
|
|
|
2273
|
-
// Create telemetry tool spans so tool calls appear in
|
|
2381
|
+
// Create telemetry tool spans so tool calls appear in traces
|
|
2274
2382
|
type ToolSpanHandle = { end: (opts: { result: { value: unknown; isError: boolean } }) => void };
|
|
2275
2383
|
const toolSpans = new Map<string, ToolSpanHandle>();
|
|
2276
2384
|
if (this.insideTelemetryCapture && this.latitudeTelemetry) {
|
|
@@ -2283,12 +2391,76 @@ ${boundedMainMemory.trim()}`
|
|
|
2283
2391
|
}),
|
|
2284
2392
|
);
|
|
2285
2393
|
}
|
|
2394
|
+
} else if (this.insideTelemetryCapture && this.hasOtlpExporter) {
|
|
2395
|
+
const tracer = trace.getTracer("poncho");
|
|
2396
|
+
for (const call of approvedCalls) {
|
|
2397
|
+
const span = tracer.startSpan(`tool ${call.name}`, {
|
|
2398
|
+
attributes: {
|
|
2399
|
+
"poncho.tool.name": call.name,
|
|
2400
|
+
"poncho.tool.call_id": call.id,
|
|
2401
|
+
"poncho.tool.arguments": JSON.stringify(call.input),
|
|
2402
|
+
},
|
|
2403
|
+
});
|
|
2404
|
+
toolSpans.set(call.id, {
|
|
2405
|
+
end(opts: { result: { value: unknown; isError: boolean } }) {
|
|
2406
|
+
if (opts.result.isError) {
|
|
2407
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(opts.result.value) });
|
|
2408
|
+
} else {
|
|
2409
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
2410
|
+
}
|
|
2411
|
+
span.end();
|
|
2412
|
+
},
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2286
2415
|
}
|
|
2287
2416
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2417
|
+
// Race tool execution against the soft deadline so long-running tool
|
|
2418
|
+
// batches (e.g. 4 parallel web_search calls) can't push us past the
|
|
2419
|
+
// hard platform timeout. If the deadline fires first, we checkpoint
|
|
2420
|
+
// with the pre-tool messages and the step will be re-done on
|
|
2421
|
+
// continuation (assistant + tool results are not yet in `messages`).
|
|
2422
|
+
const TOOL_DEADLINE_SENTINEL = Symbol("tool_deadline");
|
|
2423
|
+
const toolDeadlineRemainingMs = softDeadlineMs > 0
|
|
2424
|
+
? softDeadlineMs - (now() - start)
|
|
2425
|
+
: Infinity;
|
|
2426
|
+
|
|
2427
|
+
let batchResults: Awaited<ReturnType<typeof this.dispatcher.executeBatch>>;
|
|
2428
|
+
if (approvedCalls.length === 0) {
|
|
2429
|
+
batchResults = [];
|
|
2430
|
+
} else if (toolDeadlineRemainingMs <= 0) {
|
|
2431
|
+
batchResults = TOOL_DEADLINE_SENTINEL as never;
|
|
2432
|
+
} else if (toolDeadlineRemainingMs < Infinity) {
|
|
2433
|
+
const raced = await Promise.race([
|
|
2434
|
+
this.dispatcher.executeBatch(approvedCalls, toolContext),
|
|
2435
|
+
new Promise<typeof TOOL_DEADLINE_SENTINEL>((resolve) =>
|
|
2436
|
+
setTimeout(() => resolve(TOOL_DEADLINE_SENTINEL), toolDeadlineRemainingMs),
|
|
2437
|
+
),
|
|
2438
|
+
]);
|
|
2439
|
+
if (raced === TOOL_DEADLINE_SENTINEL) {
|
|
2440
|
+
batchResults = TOOL_DEADLINE_SENTINEL as never;
|
|
2441
|
+
} else {
|
|
2442
|
+
batchResults = raced;
|
|
2443
|
+
}
|
|
2444
|
+
} else {
|
|
2445
|
+
batchResults = await this.dispatcher.executeBatch(approvedCalls, toolContext);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
if ((batchResults as unknown) === TOOL_DEADLINE_SENTINEL) {
|
|
2449
|
+
const result_: RunResult = {
|
|
2450
|
+
status: "completed",
|
|
2451
|
+
response: responseText + fullText,
|
|
2452
|
+
steps: step,
|
|
2453
|
+
tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
|
|
2454
|
+
duration: now() - start,
|
|
2455
|
+
continuation: true,
|
|
2456
|
+
continuationMessages: [...messages],
|
|
2457
|
+
maxSteps,
|
|
2458
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
2459
|
+
contextWindow,
|
|
2460
|
+
};
|
|
2461
|
+
yield pushEvent({ type: "run:completed", runId, result: result_ });
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2292
2464
|
|
|
2293
2465
|
if (isCancelled()) {
|
|
2294
2466
|
yield emitCancellation();
|
|
@@ -2386,6 +2558,26 @@ ${boundedMainMemory.trim()}`
|
|
|
2386
2558
|
metadata: toolMsgMeta as Message["metadata"],
|
|
2387
2559
|
});
|
|
2388
2560
|
|
|
2561
|
+
// Post-tool-execution soft deadline: long-running tool batches (e.g.
|
|
2562
|
+
// multiple web_search calls) can push past the deadline. Checkpoint
|
|
2563
|
+
// now so the platform doesn't hard-kill us before we can continue.
|
|
2564
|
+
if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
|
|
2565
|
+
const result_: RunResult = {
|
|
2566
|
+
status: "completed",
|
|
2567
|
+
response: responseText + fullText,
|
|
2568
|
+
steps: step,
|
|
2569
|
+
tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
|
|
2570
|
+
duration: now() - start,
|
|
2571
|
+
continuation: true,
|
|
2572
|
+
continuationMessages: [...messages],
|
|
2573
|
+
maxSteps,
|
|
2574
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
2575
|
+
contextWindow,
|
|
2576
|
+
};
|
|
2577
|
+
yield pushEvent({ type: "run:completed", runId, result: result_ });
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2389
2581
|
// In development, re-read AGENT.md and re-scan skills after tool
|
|
2390
2582
|
// execution so changes are available on the next step without
|
|
2391
2583
|
// requiring a server restart.
|
package/src/skill-tools.ts
CHANGED
|
@@ -244,7 +244,8 @@ export const createSkillTools = (
|
|
|
244
244
|
error: `Unknown skill: "${name}". Available skills: ${knownNames}`,
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
|
-
const
|
|
247
|
+
const projectRoot = options?.workingDir ?? process.cwd();
|
|
248
|
+
const resolved = resolveScriptPath(skill.skillDir, script, projectRoot);
|
|
248
249
|
if (
|
|
249
250
|
options?.isScriptAllowed &&
|
|
250
251
|
!options.isScriptAllowed(name, resolved.relativePath)
|
|
@@ -357,7 +358,7 @@ const collectScriptFiles = async (directory: string): Promise<string[]> => {
|
|
|
357
358
|
export const normalizeScriptPolicyPath = (relativePath: string): string => {
|
|
358
359
|
const trimmed = relativePath.trim();
|
|
359
360
|
const normalized = normalize(trimmed).split(sep).join("/");
|
|
360
|
-
if (normalized.startsWith("
|
|
361
|
+
if (normalized.startsWith("/")) {
|
|
361
362
|
throw new Error("Script path must be relative and within the allowed directory");
|
|
362
363
|
}
|
|
363
364
|
const withoutDotPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
@@ -370,10 +371,12 @@ export const normalizeScriptPolicyPath = (relativePath: string): string => {
|
|
|
370
371
|
const resolveScriptPath = (
|
|
371
372
|
baseDir: string,
|
|
372
373
|
relativePath: string,
|
|
374
|
+
containmentDir?: string,
|
|
373
375
|
): { fullPath: string; relativePath: string } => {
|
|
374
376
|
const normalized = normalizeScriptPolicyPath(relativePath);
|
|
375
377
|
const fullPath = resolve(baseDir, normalized);
|
|
376
|
-
|
|
378
|
+
const boundary = resolve(containmentDir ?? baseDir);
|
|
379
|
+
if (!fullPath.startsWith(`${boundary}${sep}`) && fullPath !== boundary) {
|
|
377
380
|
throw new Error("Script path must stay inside the allowed directory");
|
|
378
381
|
}
|
|
379
382
|
const extension = extname(fullPath).toLowerCase();
|
package/src/state.ts
CHANGED
|
@@ -71,6 +71,11 @@ export interface Conversation {
|
|
|
71
71
|
/** Harness-internal message chain preserved across continuation runs.
|
|
72
72
|
* Cleared when a run completes without continuation. */
|
|
73
73
|
_continuationMessages?: Message[];
|
|
74
|
+
/** Full structured message chain from the last harness run, including
|
|
75
|
+
* tool-call and tool-result messages the model needs for context.
|
|
76
|
+
* Unlike `_continuationMessages`, this is always set after a run
|
|
77
|
+
* and does NOT signal that a continuation is pending. */
|
|
78
|
+
_harnessMessages?: Message[];
|
|
74
79
|
createdAt: number;
|
|
75
80
|
updatedAt: number;
|
|
76
81
|
}
|
package/src/telemetry.ts
CHANGED
|
@@ -11,9 +11,22 @@ function sanitizeEventForLog(event: AgentEvent): string {
|
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface OtlpConfig {
|
|
15
|
+
url: string;
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type OtlpOption = string | OtlpConfig;
|
|
20
|
+
|
|
21
|
+
export function normalizeOtlp(opt: OtlpOption | undefined): OtlpConfig | undefined {
|
|
22
|
+
if (!opt) return undefined;
|
|
23
|
+
if (typeof opt === "string") return opt ? { url: opt } : undefined;
|
|
24
|
+
return opt.url ? opt : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
export interface TelemetryConfig {
|
|
15
28
|
enabled?: boolean;
|
|
16
|
-
otlp?:
|
|
29
|
+
otlp?: OtlpOption;
|
|
17
30
|
latitude?: {
|
|
18
31
|
apiKeyEnv?: string;
|
|
19
32
|
projectIdEnv?: string;
|
|
@@ -38,8 +51,9 @@ export class TelemetryEmitter {
|
|
|
38
51
|
await this.config.handler(event);
|
|
39
52
|
return;
|
|
40
53
|
}
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
const otlp = normalizeOtlp(this.config?.otlp);
|
|
55
|
+
if (otlp) {
|
|
56
|
+
await this.sendOtlp(event, otlp);
|
|
43
57
|
}
|
|
44
58
|
// Latitude telemetry is handled by LatitudeTelemetry (from
|
|
45
59
|
// @latitude-data/telemetry) via harness.runWithTelemetry().
|
|
@@ -48,15 +62,11 @@ export class TelemetryEmitter {
|
|
|
48
62
|
process.stdout.write(`[event] ${event.type} ${sanitizeEventForLog(event)}\n`);
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
private async sendOtlp(event: AgentEvent): Promise<void> {
|
|
52
|
-
const endpoint = this.config?.otlp;
|
|
53
|
-
if (!endpoint) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
65
|
+
private async sendOtlp(event: AgentEvent, otlp: OtlpConfig): Promise<void> {
|
|
56
66
|
try {
|
|
57
|
-
await fetch(
|
|
67
|
+
await fetch(otlp.url, {
|
|
58
68
|
method: "POST",
|
|
59
|
-
headers: { "Content-Type": "application/json" },
|
|
69
|
+
headers: { "Content-Type": "application/json", ...otlp.headers },
|
|
60
70
|
body: JSON.stringify({
|
|
61
71
|
resourceLogs: [
|
|
62
72
|
{
|