@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.
- package/CHANGELOG.md +110 -0
- package/LICENSE +13 -0
- package/README.md +142 -0
- package/agent/index.ts +47 -0
- package/bridge/index.ts +10 -0
- package/dist/agent/index.d.ts +1521 -0
- package/dist/agent/index.js +2958 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bridge/index.d.ts +111 -0
- package/dist/bridge/index.js +415 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/index.d.ts +1536 -0
- package/dist/index.js +15834 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.d.ts +225 -0
- package/dist/utils/index.js +12148 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +99 -1
- package/src/agent/harness-agent-session.ts +509 -0
- package/src/agent/harness-agent-settings.ts +131 -0
- package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
- package/src/agent/harness-agent-types.ts +50 -0
- package/src/agent/harness-agent.ts +819 -0
- package/src/agent/internal/bootstrap-recipe.ts +124 -0
- package/src/agent/internal/bridge-port-registry.ts +52 -0
- package/src/agent/internal/harness-stream-text-result.ts +720 -0
- package/src/agent/internal/lifecycle-state-validation.ts +95 -0
- package/src/agent/internal/permission-mode.ts +50 -0
- package/src/agent/internal/resolve-observability.ts +128 -0
- package/src/agent/internal/run-prompt.ts +813 -0
- package/src/agent/internal/strip-work-dir.ts +68 -0
- package/src/agent/internal/to-harness-stream.ts +75 -0
- package/src/agent/internal/translate-stream-part.ts +221 -0
- package/src/agent/internal/turn-telemetry.ts +359 -0
- package/src/agent/observability/file-reporter.ts +206 -0
- package/src/agent/observability/index.ts +15 -0
- package/src/agent/observability/trace-tree-reporter.ts +122 -0
- package/src/agent/observability/types.ts +86 -0
- package/src/agent/prewarm.ts +47 -0
- package/src/bridge/index.ts +702 -0
- package/src/errors/harness-capability-unsupported-error.ts +41 -0
- package/src/errors/harness-error.ts +22 -0
- package/src/index.ts +3 -0
- package/src/utils/bridge-ready.ts +277 -0
- package/src/utils/classify-disk-log.ts +43 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/sandbox-channel.ts +453 -0
- package/src/v1/harness-v1-bootstrap.ts +46 -0
- package/src/v1/harness-v1-bridge-protocol.ts +310 -0
- package/src/v1/harness-v1-builtin-tool.ts +138 -0
- package/src/v1/harness-v1-call-warning.ts +22 -0
- package/src/v1/harness-v1-diagnostic.ts +66 -0
- package/src/v1/harness-v1-lifecycle-state.ts +65 -0
- package/src/v1/harness-v1-metadata.ts +13 -0
- package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
- package/src/v1/harness-v1-observability.ts +20 -0
- package/src/v1/harness-v1-permission-mode.ts +11 -0
- package/src/v1/harness-v1-prompt-control.ts +41 -0
- package/src/v1/harness-v1-prompt.ts +11 -0
- package/src/v1/harness-v1-sandbox-provider.ts +76 -0
- package/src/v1/harness-v1-session.ts +272 -0
- package/src/v1/harness-v1-skill.ts +36 -0
- package/src/v1/harness-v1-stream-part.ts +363 -0
- package/src/v1/harness-v1-tool-spec.ts +31 -0
- package/src/v1/harness-v1.ts +83 -0
- package/src/v1/index.ts +93 -0
- 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
|
+
}
|