@ai-sdk/harness 0.0.0 → 1.0.0-beta.14

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +13 -0
  3. package/README.md +142 -0
  4. package/agent/index.ts +47 -0
  5. package/bridge/index.ts +10 -0
  6. package/dist/agent/index.d.ts +1521 -0
  7. package/dist/agent/index.js +2958 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/bridge/index.d.ts +111 -0
  10. package/dist/bridge/index.js +415 -0
  11. package/dist/bridge/index.js.map +1 -0
  12. package/dist/index.d.ts +1536 -0
  13. package/dist/index.js +15834 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/index.d.ts +225 -0
  16. package/dist/utils/index.js +12148 -0
  17. package/dist/utils/index.js.map +1 -0
  18. package/package.json +99 -1
  19. package/src/agent/harness-agent-session.ts +509 -0
  20. package/src/agent/harness-agent-settings.ts +131 -0
  21. package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
  22. package/src/agent/harness-agent-types.ts +50 -0
  23. package/src/agent/harness-agent.ts +819 -0
  24. package/src/agent/internal/bootstrap-recipe.ts +124 -0
  25. package/src/agent/internal/bridge-port-registry.ts +52 -0
  26. package/src/agent/internal/harness-stream-text-result.ts +720 -0
  27. package/src/agent/internal/lifecycle-state-validation.ts +95 -0
  28. package/src/agent/internal/permission-mode.ts +50 -0
  29. package/src/agent/internal/resolve-observability.ts +128 -0
  30. package/src/agent/internal/run-prompt.ts +813 -0
  31. package/src/agent/internal/strip-work-dir.ts +68 -0
  32. package/src/agent/internal/to-harness-stream.ts +75 -0
  33. package/src/agent/internal/translate-stream-part.ts +221 -0
  34. package/src/agent/internal/turn-telemetry.ts +359 -0
  35. package/src/agent/observability/file-reporter.ts +206 -0
  36. package/src/agent/observability/index.ts +15 -0
  37. package/src/agent/observability/trace-tree-reporter.ts +122 -0
  38. package/src/agent/observability/types.ts +86 -0
  39. package/src/agent/prewarm.ts +47 -0
  40. package/src/bridge/index.ts +702 -0
  41. package/src/errors/harness-capability-unsupported-error.ts +41 -0
  42. package/src/errors/harness-error.ts +22 -0
  43. package/src/index.ts +3 -0
  44. package/src/utils/bridge-ready.ts +277 -0
  45. package/src/utils/classify-disk-log.ts +43 -0
  46. package/src/utils/index.ts +15 -0
  47. package/src/utils/sandbox-channel.ts +453 -0
  48. package/src/v1/harness-v1-bootstrap.ts +46 -0
  49. package/src/v1/harness-v1-bridge-protocol.ts +310 -0
  50. package/src/v1/harness-v1-builtin-tool.ts +138 -0
  51. package/src/v1/harness-v1-call-warning.ts +22 -0
  52. package/src/v1/harness-v1-diagnostic.ts +66 -0
  53. package/src/v1/harness-v1-lifecycle-state.ts +65 -0
  54. package/src/v1/harness-v1-metadata.ts +13 -0
  55. package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
  56. package/src/v1/harness-v1-observability.ts +20 -0
  57. package/src/v1/harness-v1-permission-mode.ts +11 -0
  58. package/src/v1/harness-v1-prompt-control.ts +41 -0
  59. package/src/v1/harness-v1-prompt.ts +11 -0
  60. package/src/v1/harness-v1-sandbox-provider.ts +76 -0
  61. package/src/v1/harness-v1-session.ts +272 -0
  62. package/src/v1/harness-v1-skill.ts +36 -0
  63. package/src/v1/harness-v1-stream-part.ts +363 -0
  64. package/src/v1/harness-v1-tool-spec.ts +31 -0
  65. package/src/v1/harness-v1.ts +83 -0
  66. package/src/v1/index.ts +93 -0
  67. package/utils/index.ts +1 -0
@@ -0,0 +1,95 @@
1
+ import { safeValidateTypes } from '@ai-sdk/provider-utils';
2
+ import { HarnessError } from '../../errors/harness-error';
3
+ import type { HarnessV1, HarnessV1LifecycleState } from '../../v1';
4
+
5
+ /**
6
+ * Validate a lifecycle state against the harness's contract:
7
+ * - `type` must match the lifecycle method that will consume it.
8
+ * - `specificationVersion` must be `'harness-v1'`.
9
+ * - `harnessId` must match the harness producing/consuming the payload.
10
+ * - When the harness declares a `lifecycleStateSchema`, `data` is parsed
11
+ * against it.
12
+ *
13
+ * Returns the payload with `data` replaced by the parsed value when a
14
+ * schema is present, so callers downstream see a canonical shape.
15
+ */
16
+ export async function validateLifecycleStateData<
17
+ STATE extends HarnessV1LifecycleState,
18
+ >(input: {
19
+ harness: HarnessV1;
20
+ state: STATE;
21
+ expectedType: STATE['type'];
22
+ }): Promise<STATE> {
23
+ const { harness, state } = input;
24
+ if (state.type !== input.expectedType) {
25
+ throw new HarnessError({
26
+ message: `Lifecycle state has unexpected type '${state.type}'; expected '${input.expectedType}'.`,
27
+ });
28
+ }
29
+ if (state.specificationVersion !== 'harness-v1') {
30
+ throw new HarnessError({
31
+ message: `Lifecycle state has unexpected specificationVersion '${state.specificationVersion}'; expected 'harness-v1'.`,
32
+ });
33
+ }
34
+ if (state.harnessId !== harness.harnessId) {
35
+ throw new HarnessError({
36
+ message: `Lifecycle state was produced by harness '${state.harnessId}' but this agent uses '${harness.harnessId}'.`,
37
+ });
38
+ }
39
+ if (
40
+ state.type === 'resume-session' &&
41
+ 'pendingToolApprovals' in state &&
42
+ state.pendingToolApprovals !== undefined
43
+ ) {
44
+ throw new HarnessError({
45
+ message:
46
+ 'Resume session state cannot contain pending tool approvals; unfinished turns must be stored as `continueFrom`.',
47
+ });
48
+ }
49
+
50
+ const data =
51
+ harness.lifecycleStateSchema == null
52
+ ? state.data
53
+ : await (async () => {
54
+ const result = await safeValidateTypes({
55
+ value: state.data,
56
+ schema: harness.lifecycleStateSchema!,
57
+ });
58
+ if (!result.success) {
59
+ throw new HarnessError({
60
+ message: `Lifecycle state failed schema validation for harness '${harness.harnessId}': ${result.error.message}`,
61
+ cause: result.error,
62
+ });
63
+ }
64
+ return result.value as STATE['data'];
65
+ })();
66
+
67
+ if (state.type === 'resume-session') {
68
+ const continueFrom =
69
+ state.continueFrom == null
70
+ ? undefined
71
+ : await validateLifecycleStateData({
72
+ harness,
73
+ state: state.continueFrom,
74
+ expectedType: 'continue-turn',
75
+ });
76
+
77
+ return {
78
+ type: state.type,
79
+ harnessId: state.harnessId,
80
+ specificationVersion: state.specificationVersion,
81
+ data,
82
+ ...(continueFrom !== undefined ? { continueFrom } : {}),
83
+ } as STATE;
84
+ }
85
+
86
+ return {
87
+ type: state.type,
88
+ harnessId: state.harnessId,
89
+ specificationVersion: state.specificationVersion,
90
+ data,
91
+ ...(state.pendingToolApprovals !== undefined
92
+ ? { pendingToolApprovals: state.pendingToolApprovals }
93
+ : {}),
94
+ } as STATE;
95
+ }
@@ -0,0 +1,50 @@
1
+ import type { ToolApprovalStatus } from 'ai';
2
+ import type { HarnessV1PermissionMode } from '../../v1';
3
+ import type { HarnessAgentToolApprovalConfiguration } from '../harness-agent-settings';
4
+
5
+ export const DEFAULT_PERMISSION_MODE: HarnessV1PermissionMode =
6
+ 'allow-all' as const;
7
+
8
+ export function resolvePermissionMode(input: {
9
+ permissionMode: HarnessV1PermissionMode | undefined;
10
+ }): HarnessV1PermissionMode {
11
+ return input.permissionMode ?? DEFAULT_PERMISSION_MODE;
12
+ }
13
+
14
+ export function permissionModeNeedsBuiltinSupport(input: {
15
+ permissionMode: HarnessV1PermissionMode;
16
+ }): boolean {
17
+ return input.permissionMode !== 'allow-all';
18
+ }
19
+
20
+ export type CustomToolApprovalDecision =
21
+ | { readonly type: 'allow'; readonly reason?: string }
22
+ | { readonly type: 'deny'; readonly reason?: string }
23
+ | { readonly type: 'request' };
24
+
25
+ export function resolveCustomToolApproval(input: {
26
+ toolName: string;
27
+ toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
28
+ }): CustomToolApprovalDecision {
29
+ const status = normalizeToolApprovalStatus({
30
+ status: input.toolApproval?.[input.toolName],
31
+ });
32
+
33
+ switch (status.type) {
34
+ case 'not-applicable':
35
+ case 'approved':
36
+ return { type: 'allow', reason: status.reason };
37
+ case 'denied':
38
+ return { type: 'deny', reason: status.reason };
39
+ case 'user-approval':
40
+ return { type: 'request' };
41
+ }
42
+ }
43
+
44
+ function normalizeToolApprovalStatus(input: {
45
+ status: ToolApprovalStatus | undefined;
46
+ }): Exclude<ToolApprovalStatus, string | undefined> {
47
+ if (input.status === undefined) return { type: 'not-applicable' };
48
+ if (typeof input.status === 'string') return { type: input.status };
49
+ return input.status;
50
+ }
@@ -0,0 +1,128 @@
1
+ import { asArray } from '@ai-sdk/provider-utils';
2
+ import type { Telemetry } from 'ai';
3
+ import type { HarnessV1Diagnostic, HarnessV1Observability } from '../../v1';
4
+ import type {
5
+ HarnessDebugConfig,
6
+ HarnessDebugLevel,
7
+ HarnessDiagnostic,
8
+ HarnessDiagnosticConsumer,
9
+ } from '../observability/types';
10
+ import type { HarnessAgentSettings } from '../harness-agent-settings';
11
+
12
+ const ENV_TRUTHY = new Set(['1', 'true', 'yes', 'on']);
13
+
14
+ function parseList(value: string | undefined): string[] | undefined {
15
+ if (!value) return undefined;
16
+ const items = value
17
+ .split(',')
18
+ .map(item => item.trim())
19
+ .filter(Boolean);
20
+ return items.length > 0 ? items : undefined;
21
+ }
22
+
23
+ /**
24
+ * Resolve the effective debug config from explicit settings with env-var
25
+ * fallbacks. `HARNESS_DEBUG*` only fill fields the consumer left unset — code is
26
+ * always authoritative.
27
+ */
28
+ export function resolveDebugConfig(
29
+ debug: HarnessDebugConfig | undefined,
30
+ env: Record<string, string | undefined> = process.env,
31
+ ): {
32
+ enabled: boolean;
33
+ level: HarnessDebugLevel;
34
+ subsystems: string[] | undefined;
35
+ } {
36
+ const enabled =
37
+ debug?.enabled ?? ENV_TRUTHY.has((env.HARNESS_DEBUG ?? '').toLowerCase());
38
+ const level =
39
+ debug?.level ??
40
+ (env.HARNESS_DEBUG_LEVEL as HarnessDebugLevel | undefined) ??
41
+ 'debug';
42
+ const subsystems = debug?.subsystems
43
+ ? [...debug.subsystems]
44
+ : parseList(env.HARNESS_DEBUG_SUBSYSTEMS);
45
+ return { enabled, level, subsystems };
46
+ }
47
+
48
+ /**
49
+ * Map an adapter-emitted `HarnessV1Diagnostic` to the host-facing
50
+ * `HarnessDiagnostic` (the external/telemetry surface). Identity-shaped today,
51
+ * but an explicit boundary so the emission and consumption types can diverge.
52
+ */
53
+ function toHarnessDiagnostic(d: HarnessV1Diagnostic): HarnessDiagnostic {
54
+ return {
55
+ level: d.level,
56
+ message: d.message,
57
+ subsystem: d.subsystem,
58
+ kind: d.kind,
59
+ source: d.source,
60
+ stream: d.stream,
61
+ attrs: d.attrs,
62
+ error: d.error,
63
+ sessionId: d.sessionId,
64
+ timestamp: d.timestamp,
65
+ };
66
+ }
67
+
68
+ function formatForStderr(d: HarnessDiagnostic): string {
69
+ const parts = [`[harness:${d.level}]`, d.subsystem, d.message].filter(
70
+ Boolean,
71
+ );
72
+ let line = parts.join(' ');
73
+ if (d.error) {
74
+ line += ` (${d.error.name ?? 'Error'}: ${d.error.message})`;
75
+ }
76
+ return `${line}\n`;
77
+ }
78
+
79
+ /**
80
+ * Build the per-session observability handle the framework hands to `doStart`.
81
+ * Returns `undefined` when diagnostics are disabled. When enabled, the `report`
82
+ * sink maps each adapter-emitted `HarnessV1Diagnostic` to the host
83
+ * `HarnessDiagnostic` and fans it out, non-lossy, to: the `HARNESS_DEBUG` stderr
84
+ * default, the consumer's `onLog`, and any telemetry integration that
85
+ * implements `ingestDiagnostic`. Adapters normalize their own source (bridge
86
+ * wire frames, host logs) into `HarnessV1Diagnostic` before calling `report`.
87
+ */
88
+ export function buildObservability(options: {
89
+ settings: Pick<
90
+ HarnessAgentSettings<any, any>,
91
+ 'debug' | 'onLog' | 'telemetry'
92
+ >;
93
+ }): HarnessV1Observability | undefined {
94
+ const resolved = resolveDebugConfig(options.settings.debug);
95
+ if (!resolved.enabled) return undefined;
96
+
97
+ const onLog = options.settings.onLog;
98
+ const integrations = options.settings.telemetry?.integrations
99
+ ? asArray(options.settings.telemetry.integrations)
100
+ : [];
101
+ const diagnosticConsumers = integrations.filter(
102
+ (integration): integration is Telemetry & HarnessDiagnosticConsumer =>
103
+ typeof (integration as HarnessDiagnosticConsumer).ingestDiagnostic ===
104
+ 'function',
105
+ );
106
+
107
+ const report = (emitted: HarnessV1Diagnostic): void => {
108
+ const diagnostic = toHarnessDiagnostic(emitted);
109
+ try {
110
+ process.stderr.write(formatForStderr(diagnostic));
111
+ } catch {
112
+ // Never let the stderr default break the turn.
113
+ }
114
+ onLog?.(diagnostic);
115
+ for (const consumer of diagnosticConsumers) {
116
+ consumer.ingestDiagnostic?.(diagnostic);
117
+ }
118
+ };
119
+
120
+ return {
121
+ debug: {
122
+ enabled: true,
123
+ level: resolved.level,
124
+ ...(resolved.subsystems ? { subsystems: resolved.subsystems } : {}),
125
+ },
126
+ report,
127
+ };
128
+ }