@plimeor/harness 0.1.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/README.md +221 -0
- package/package.json +39 -0
- package/src/adapters/claude.ts +112 -0
- package/src/adapters/codex.ts +118 -0
- package/src/adapters/extensions.ts +1235 -0
- package/src/adapters/index.ts +9 -0
- package/src/adapters/kiro.ts +57 -0
- package/src/adapters/pi.ts +65 -0
- package/src/adapters/shared.ts +338 -0
- package/src/errors.ts +43 -0
- package/src/index.ts +34 -0
- package/src/output.ts +183 -0
- package/src/process.ts +183 -0
- package/src/registry.ts +34 -0
- package/src/schema.ts +18 -0
- package/src/types.ts +166 -0
package/src/output.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
2
|
+
|
|
3
|
+
import { HarnessRunOutputError } from './errors'
|
|
4
|
+
import type { HarnessRunEvent, HarnessRunResult, RunOutputRequest, StructuredOutputRequest } from './types'
|
|
5
|
+
|
|
6
|
+
export async function decodeRunOutput<Output extends RunOutputRequest>(input: {
|
|
7
|
+
output: Output
|
|
8
|
+
finalText: string
|
|
9
|
+
exitCode: number | null
|
|
10
|
+
signal?: string
|
|
11
|
+
}): Promise<{ events: HarnessRunEvent[]; result: HarnessRunResult<Output> }> {
|
|
12
|
+
const mode = input.output.mode ?? 'text'
|
|
13
|
+
|
|
14
|
+
if (mode === 'text') {
|
|
15
|
+
return {
|
|
16
|
+
events: input.finalText ? [{ text: input.finalText, type: 'text' }] : [],
|
|
17
|
+
result: baseResult(input) as HarnessRunResult<Output>
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (mode === 'jsonl') {
|
|
22
|
+
const values = parseJsonLines(input.finalText, input.exitCode, input.signal)
|
|
23
|
+
return {
|
|
24
|
+
events: values.map(value => ({ type: 'json', value })),
|
|
25
|
+
result: {
|
|
26
|
+
...baseResult(input),
|
|
27
|
+
finalText: extractFinalText(values) ?? input.finalText
|
|
28
|
+
} as HarnessRunResult<Output>
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (await decodeStructuredOutput(
|
|
33
|
+
input as { output: StructuredOutputRequest; finalText: string; exitCode: number | null; signal?: string }
|
|
34
|
+
)) as { events: HarnessRunEvent[]; result: HarnessRunResult<Output> }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractFinalText(values: unknown[]): string | undefined {
|
|
38
|
+
for (const value of values.toReversed()) {
|
|
39
|
+
const text = extractTextFromEvent(value)
|
|
40
|
+
if (text) {
|
|
41
|
+
return text
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractTextFromEvent(value: unknown): string | undefined {
|
|
49
|
+
if (!isRecord(value)) {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = value.result
|
|
54
|
+
if (typeof result === 'string' && result.length > 0) {
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const structuredOutput = value.structured_output
|
|
59
|
+
if (structuredOutput !== undefined) {
|
|
60
|
+
return JSON.stringify(structuredOutput)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const item = value.item
|
|
64
|
+
if (isRecord(item) && item.type === 'agent_message' && typeof item.text === 'string' && item.text.length > 0) {
|
|
65
|
+
return item.text
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const message = value.message
|
|
69
|
+
if (isRecord(message)) {
|
|
70
|
+
const messageText = extractTextFromContent(message.content)
|
|
71
|
+
if (messageText) {
|
|
72
|
+
return messageText
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractTextFromContent(content: unknown): string | undefined {
|
|
80
|
+
if (!Array.isArray(content)) {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const text = content
|
|
85
|
+
.map(part => {
|
|
86
|
+
if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {
|
|
87
|
+
return part.text
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return ''
|
|
91
|
+
})
|
|
92
|
+
.join('')
|
|
93
|
+
|
|
94
|
+
return text.length > 0 ? text : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
98
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function baseResult(input: { finalText: string; exitCode: number | null; signal?: string }): {
|
|
102
|
+
exitCode: number | null
|
|
103
|
+
signal?: string
|
|
104
|
+
finalText: string
|
|
105
|
+
} {
|
|
106
|
+
return {
|
|
107
|
+
exitCode: input.exitCode,
|
|
108
|
+
finalText: input.finalText,
|
|
109
|
+
signal: input.signal
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseJsonLines(text: string, exitCode: number | null, signal: string | undefined): unknown[] {
|
|
114
|
+
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0)
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return lines.map(line => JSON.parse(line))
|
|
118
|
+
} catch (cause) {
|
|
119
|
+
throw new HarnessRunOutputError({
|
|
120
|
+
cause,
|
|
121
|
+
exitCode,
|
|
122
|
+
finalText: text,
|
|
123
|
+
kind: 'json_parse_failed',
|
|
124
|
+
outputMode: 'jsonl',
|
|
125
|
+
signal
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function decodeStructuredOutput<Schema extends StandardSchemaV1>(input: {
|
|
131
|
+
output: StructuredOutputRequest<Schema>
|
|
132
|
+
finalText: string
|
|
133
|
+
exitCode: number | null
|
|
134
|
+
signal?: string
|
|
135
|
+
}): Promise<{
|
|
136
|
+
events: HarnessRunEvent[]
|
|
137
|
+
result: HarnessRunResult<StructuredOutputRequest<Schema>>
|
|
138
|
+
}> {
|
|
139
|
+
let value: unknown
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
value = JSON.parse(input.finalText)
|
|
143
|
+
} catch (cause) {
|
|
144
|
+
throw new HarnessRunOutputError({
|
|
145
|
+
cause,
|
|
146
|
+
exitCode: input.exitCode,
|
|
147
|
+
finalText: input.finalText,
|
|
148
|
+
kind: 'json_parse_failed',
|
|
149
|
+
outputMode: 'structured',
|
|
150
|
+
signal: input.signal
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const structuredValue = extractStructuredValue(value)
|
|
155
|
+
const validation = await input.output.schema['~standard'].validate(structuredValue)
|
|
156
|
+
if (validation.issues) {
|
|
157
|
+
throw new HarnessRunOutputError({
|
|
158
|
+
cause: validation.issues,
|
|
159
|
+
exitCode: input.exitCode,
|
|
160
|
+
finalText: input.finalText,
|
|
161
|
+
kind: 'structured_validation_failed',
|
|
162
|
+
outputMode: 'structured',
|
|
163
|
+
signal: input.signal
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
events: [{ type: 'json', value }],
|
|
169
|
+
result: {
|
|
170
|
+
...baseResult(input),
|
|
171
|
+
finalText: extractTextFromEvent(value) ?? input.finalText,
|
|
172
|
+
structured: validation.value as StandardSchemaV1.InferOutput<Schema>
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function extractStructuredValue(value: unknown): unknown {
|
|
178
|
+
if (isRecord(value) && value.structured_output !== undefined) {
|
|
179
|
+
return value.structured_output
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return value
|
|
183
|
+
}
|
package/src/process.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { decodeRunOutput } from './output'
|
|
2
|
+
import type { CommandPlan, HarnessId, HarnessRun, HarnessRunEvent, RunOutputRequest } from './types'
|
|
3
|
+
|
|
4
|
+
class AsyncQueue<T> implements AsyncIterable<T> {
|
|
5
|
+
private readonly values: T[] = []
|
|
6
|
+
private readonly waiters: Array<(result: IteratorResult<T>) => void> = []
|
|
7
|
+
private closed = false
|
|
8
|
+
|
|
9
|
+
push(value: T): void {
|
|
10
|
+
const waiter = this.waiters.shift()
|
|
11
|
+
if (waiter) {
|
|
12
|
+
waiter({ done: false, value })
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.values.push(value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
close(): void {
|
|
20
|
+
this.closed = true
|
|
21
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
22
|
+
waiter({ done: true, value: undefined })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
27
|
+
return {
|
|
28
|
+
next: () => this.next()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private next(): Promise<IteratorResult<T>> {
|
|
33
|
+
const value = this.values.shift()
|
|
34
|
+
if (value !== undefined) {
|
|
35
|
+
return Promise.resolve({ done: false, value })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (this.closed) {
|
|
39
|
+
return Promise.resolve({ done: true, value: undefined })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Promise(resolve => {
|
|
43
|
+
this.waiters.push(resolve)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function runCommandPlan<Output extends RunOutputRequest>(
|
|
49
|
+
harnessId: HarnessId,
|
|
50
|
+
plan: CommandPlan<Output>
|
|
51
|
+
): HarnessRun<Output> {
|
|
52
|
+
if (plan.harnessId !== harnessId) {
|
|
53
|
+
throw new Error(`CommandPlan harnessId mismatch: expected ${harnessId}, received ${plan.harnessId}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stdout = new AsyncQueue<Uint8Array>()
|
|
57
|
+
const stderr = new AsyncQueue<Uint8Array>()
|
|
58
|
+
const events = new AsyncQueue<HarnessRunEvent>()
|
|
59
|
+
const decoder = new TextDecoder()
|
|
60
|
+
let stdoutText = ''
|
|
61
|
+
|
|
62
|
+
const subprocess = Bun.spawn({
|
|
63
|
+
cmd: [plan.command, ...plan.args],
|
|
64
|
+
cwd: plan.cwd,
|
|
65
|
+
env: resolveEnv(plan.env),
|
|
66
|
+
stderr: 'pipe',
|
|
67
|
+
stdin: plan.stdin === undefined ? 'ignore' : 'pipe',
|
|
68
|
+
stdout: 'pipe'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (plan.stdin !== undefined && subprocess.stdin) {
|
|
72
|
+
void writeStdin(subprocess.stdin, plan.stdin)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const timeout = plan.timeoutMs
|
|
76
|
+
? setTimeout(() => {
|
|
77
|
+
subprocess.kill()
|
|
78
|
+
}, plan.timeoutMs)
|
|
79
|
+
: undefined
|
|
80
|
+
|
|
81
|
+
const stdoutDone = pipeStream(subprocess.stdout, stdout, chunk => {
|
|
82
|
+
const text = decoder.decode(chunk, { stream: true })
|
|
83
|
+
stdoutText += text
|
|
84
|
+
})
|
|
85
|
+
const stderrDone = pipeStream(subprocess.stderr, stderr)
|
|
86
|
+
|
|
87
|
+
const result = (async () => {
|
|
88
|
+
const exitCode = await subprocess.exited
|
|
89
|
+
clearTimeout(timeout)
|
|
90
|
+
await Promise.all([stdoutDone, stderrDone])
|
|
91
|
+
const tail = decoder.decode()
|
|
92
|
+
stdoutText += tail
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const decoded = await decodeRunOutput({
|
|
96
|
+
exitCode,
|
|
97
|
+
finalText: stdoutText,
|
|
98
|
+
output: plan.output,
|
|
99
|
+
signal: subprocess.signalCode ?? undefined
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
for (const event of decoded.events) {
|
|
103
|
+
events.push(event)
|
|
104
|
+
}
|
|
105
|
+
return decoded.result
|
|
106
|
+
} finally {
|
|
107
|
+
events.close()
|
|
108
|
+
}
|
|
109
|
+
})()
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
events,
|
|
113
|
+
kill(signal?: string) {
|
|
114
|
+
subprocess.kill(signal as NodeJS.Signals | undefined)
|
|
115
|
+
},
|
|
116
|
+
plan,
|
|
117
|
+
result,
|
|
118
|
+
stderr,
|
|
119
|
+
stdout
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createProcessRunner(harnessId: HarnessId): {
|
|
124
|
+
run<Output extends RunOutputRequest>(plan: CommandPlan<Output>): Promise<HarnessRun<Output>>
|
|
125
|
+
} {
|
|
126
|
+
return {
|
|
127
|
+
async run<Output extends RunOutputRequest>(plan: CommandPlan<Output>): Promise<HarnessRun<Output>> {
|
|
128
|
+
return runCommandPlan(harnessId, plan)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveEnv(patch: Record<string, string | undefined> | undefined): Record<string, string> {
|
|
134
|
+
const env: Record<string, string> = {}
|
|
135
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
136
|
+
if (typeof value === 'string') {
|
|
137
|
+
env[name] = value
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const [name, value] of Object.entries(patch ?? {})) {
|
|
142
|
+
if (value === undefined) {
|
|
143
|
+
delete env[name]
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
env[name] = value
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return env
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function pipeStream(
|
|
154
|
+
stream: ReadableStream<Uint8Array> | null,
|
|
155
|
+
queue: AsyncQueue<Uint8Array>,
|
|
156
|
+
onChunk?: (chunk: Uint8Array) => void
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
if (!stream) {
|
|
159
|
+
queue.close()
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const reader = stream.getReader()
|
|
164
|
+
try {
|
|
165
|
+
while (true) {
|
|
166
|
+
const chunk = await reader.read()
|
|
167
|
+
if (chunk.done) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
queue.push(chunk.value)
|
|
172
|
+
onChunk?.(chunk.value)
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
queue.close()
|
|
176
|
+
reader.releaseLock()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function writeStdin(stdin: Bun.FileSink, value: string | Uint8Array): Promise<void> {
|
|
181
|
+
stdin.write(value)
|
|
182
|
+
stdin.end()
|
|
183
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HarnessAdapter, HarnessContext, HarnessDetection, HarnessId, HarnessRegistry } from './types'
|
|
2
|
+
|
|
3
|
+
function createHarnessRegistry(): HarnessRegistry {
|
|
4
|
+
const adapters: HarnessAdapter[] = []
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
async detectAll(context?: HarnessContext): Promise<HarnessDetection[]> {
|
|
8
|
+
return Promise.all(adapters.map(adapter => adapter.detect(context)))
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
list(): HarnessAdapter[] {
|
|
12
|
+
return [...adapters]
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async open(id: HarnessId, context?: HarnessContext) {
|
|
16
|
+
const adapter = adapters.find(candidate => candidate.id === id)
|
|
17
|
+
if (!adapter) {
|
|
18
|
+
throw new Error(`Unknown harness adapter: ${id}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return adapter.open(context)
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
use(adapter: HarnessAdapter): void {
|
|
25
|
+
if (adapters.some(candidate => candidate.id === adapter.id)) {
|
|
26
|
+
throw new Error(`Duplicate harness adapter: ${adapter.id}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
adapters.push(adapter)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const harness = createHarnessRegistry()
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec'
|
|
2
|
+
|
|
3
|
+
export function resolveOutputJsonSchema(schema: StandardSchemaV1): Record<string, unknown> | undefined {
|
|
4
|
+
if (!isStandardJsonSchema(schema)) {
|
|
5
|
+
return undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
return schema['~standard'].jsonSchema.output({ target: 'draft-07' })
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isStandardJsonSchema(schema: StandardSchemaV1): schema is StandardSchemaV1 & StandardJSONSchemaV1 {
|
|
16
|
+
const candidate = schema as StandardSchemaV1 & Partial<StandardJSONSchemaV1>
|
|
17
|
+
return typeof candidate['~standard'].jsonSchema?.output === 'function'
|
|
18
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
2
|
+
|
|
3
|
+
export type HarnessId = 'claude' | 'codex' | 'kiro' | 'pi' | (string & {})
|
|
4
|
+
|
|
5
|
+
export type HarnessContext = {
|
|
6
|
+
/** Default working directory for adapter operations; relative extension paths resolve from here. */
|
|
7
|
+
cwd?: string
|
|
8
|
+
/** Base environment available to detection, planning, and adapter-owned native commands. */
|
|
9
|
+
env?: Record<string, string | undefined>
|
|
10
|
+
/** User home directory used for user-scope native config resolution. */
|
|
11
|
+
home?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type HarnessRegistry = {
|
|
15
|
+
use(adapter: HarnessAdapter): void
|
|
16
|
+
list(): HarnessAdapter[]
|
|
17
|
+
detectAll(context?: HarnessContext): Promise<HarnessDetection[]>
|
|
18
|
+
open(id: HarnessId, context?: HarnessContext): Promise<HarnessHandle>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type HarnessAdapter = {
|
|
22
|
+
id: HarnessId
|
|
23
|
+
detect(context?: HarnessContext): Promise<HarnessDetection>
|
|
24
|
+
open(context?: HarnessContext): Promise<HarnessHandle>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type HarnessHandle = {
|
|
28
|
+
detection: HarnessDetection
|
|
29
|
+
health: HealthFacet
|
|
30
|
+
process: ProcessFacet
|
|
31
|
+
extensions: ExtensionFacet
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type HarnessDetection = {
|
|
35
|
+
id: HarnessId
|
|
36
|
+
detected: boolean
|
|
37
|
+
binary?: {
|
|
38
|
+
/** Executable name or path used to invoke the detected harness CLI. */
|
|
39
|
+
command: string
|
|
40
|
+
/** Adapter-verified identity string, such as a version or product marker. */
|
|
41
|
+
identity?: string
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type HealthFacet = {
|
|
46
|
+
check(): Promise<HealthReport>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type HealthReport = { success: true } | { success: false; message: string }
|
|
50
|
+
|
|
51
|
+
export type ProcessFacet = {
|
|
52
|
+
plan<Output extends RunOutputRequest = TextOutputRequest>(request: RunRequest<Output>): Promise<CommandPlan<Output>>
|
|
53
|
+
run<Output extends RunOutputRequest = TextOutputRequest>(request: RunRequest<Output>): Promise<HarnessRun<Output>>
|
|
54
|
+
run<Output extends RunOutputRequest = TextOutputRequest>(plan: CommandPlan<Output>): Promise<HarnessRun<Output>>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type RunRequest<Output extends RunOutputRequest = TextOutputRequest> = {
|
|
58
|
+
prompt: string
|
|
59
|
+
/** Requested working directory before adapter resolution. */
|
|
60
|
+
cwd?: string
|
|
61
|
+
stdin?: string | Uint8Array
|
|
62
|
+
/** Process environment patch requested by the caller for this run. */
|
|
63
|
+
env?: Record<string, string | undefined>
|
|
64
|
+
/** Defaults to text output when omitted. */
|
|
65
|
+
output?: Output
|
|
66
|
+
timeoutMs?: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type RunOutputRequest = TextOutputRequest | JsonlOutputRequest | StructuredOutputRequest
|
|
70
|
+
|
|
71
|
+
export type TextOutputRequest = { mode?: 'text' }
|
|
72
|
+
|
|
73
|
+
export type JsonlOutputRequest = { mode: 'jsonl' }
|
|
74
|
+
|
|
75
|
+
export type StructuredOutputRequest<Schema extends StandardSchemaV1 = StandardSchemaV1> = {
|
|
76
|
+
mode: 'structured'
|
|
77
|
+
schema: Schema
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type CommandPlan<Output extends RunOutputRequest = TextOutputRequest> = {
|
|
81
|
+
harnessId: HarnessId
|
|
82
|
+
/** Executable name or path to spawn; arguments stay in args. */
|
|
83
|
+
command: string
|
|
84
|
+
args: string[]
|
|
85
|
+
/** Resolved working directory used by process.run. */
|
|
86
|
+
cwd: string
|
|
87
|
+
/** Process environment patch: string sets a variable, undefined removes it. */
|
|
88
|
+
env?: Record<string, string | undefined>
|
|
89
|
+
stdin?: string | Uint8Array
|
|
90
|
+
output: Output
|
|
91
|
+
timeoutMs?: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type HarnessRun<Output extends RunOutputRequest = TextOutputRequest> = {
|
|
95
|
+
plan: CommandPlan<Output>
|
|
96
|
+
stdout: AsyncIterable<Uint8Array>
|
|
97
|
+
stderr: AsyncIterable<Uint8Array>
|
|
98
|
+
events: AsyncIterable<HarnessRunEvent>
|
|
99
|
+
result: Promise<HarnessRunResult<Output>>
|
|
100
|
+
kill(signal?: string): void
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type HarnessRunEvent = { type: 'text'; text: string } | { type: 'json'; value: unknown }
|
|
104
|
+
|
|
105
|
+
export type HarnessRunResult<Output extends RunOutputRequest = TextOutputRequest> = {
|
|
106
|
+
exitCode: number | null
|
|
107
|
+
signal?: string
|
|
108
|
+
finalText: string
|
|
109
|
+
} & StructuredRunResult<Output>
|
|
110
|
+
|
|
111
|
+
export type StructuredRunResult<Output extends RunOutputRequest> =
|
|
112
|
+
Output extends StructuredOutputRequest<infer Schema>
|
|
113
|
+
? { structured: StandardSchemaV1.InferOutput<Schema> }
|
|
114
|
+
: { structured?: never }
|
|
115
|
+
|
|
116
|
+
export type ExtensionFacet = {
|
|
117
|
+
check(extension: HarnessExtension): Promise<ExtensionCheckResult>
|
|
118
|
+
install(extension: HarnessExtension): Promise<ExtensionResult>
|
|
119
|
+
uninstall(extensionId: string): Promise<ExtensionResult>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type HarnessExtension = {
|
|
123
|
+
/** Stable extension id used by adapter-owned install records and uninstall. */
|
|
124
|
+
id: string
|
|
125
|
+
resources: ExtensionResources
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type ExtensionResources = {
|
|
129
|
+
/** Filesystem paths to skill files or directories; relative paths resolve from HarnessContext.cwd. */
|
|
130
|
+
skills?: string[]
|
|
131
|
+
hooks?: HookResource[]
|
|
132
|
+
mcpServers?: Record<string, McpServerResource>
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ExtensionResourceKind = keyof ExtensionResources
|
|
136
|
+
|
|
137
|
+
export type McpServerResource = {
|
|
138
|
+
/** Executable name or path for the stdio MCP server; arguments stay in args. */
|
|
139
|
+
command: string
|
|
140
|
+
args?: string[]
|
|
141
|
+
/** Environment variables added for this MCP server process. */
|
|
142
|
+
env?: Record<string, string>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type HookResource = {
|
|
146
|
+
/** Stable hook resource name within this extension. */
|
|
147
|
+
name: string
|
|
148
|
+
/** Native hook event name validated by the adapter. */
|
|
149
|
+
event: string
|
|
150
|
+
/** Command string installed into the target harness native hook config. */
|
|
151
|
+
command: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type ExtensionResult = {
|
|
155
|
+
success: boolean
|
|
156
|
+
issues: ExtensionIssue[]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type ExtensionCheckResult = {
|
|
160
|
+
compatible: boolean
|
|
161
|
+
issues: ExtensionIssue[]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type ExtensionIssue =
|
|
165
|
+
| { kind: 'unsupported'; resourceKind: ExtensionResourceKind; resourceName?: string; reason: string }
|
|
166
|
+
| { kind: 'conflict'; resourceKind?: ExtensionResourceKind; resourceName?: string; reason: string }
|