@jmoyers/harness 0.1.8 → 0.1.10

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 (41) hide show
  1. package/README.md +33 -155
  2. package/package.json +5 -1
  3. package/packages/harness-ai/src/anthropic-client.ts +99 -0
  4. package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
  5. package/packages/harness-ai/src/anthropic-provider.ts +82 -0
  6. package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
  7. package/packages/harness-ai/src/index.ts +36 -0
  8. package/packages/harness-ai/src/json-parse.ts +66 -0
  9. package/packages/harness-ai/src/sse.ts +80 -0
  10. package/packages/harness-ai/src/stream-object.ts +96 -0
  11. package/packages/harness-ai/src/stream-text.ts +1340 -0
  12. package/packages/harness-ai/src/types.ts +330 -0
  13. package/packages/harness-ai/src/ui-stream.ts +217 -0
  14. package/scripts/codex-live-mux-runtime.ts +123 -7
  15. package/scripts/control-plane-daemon.ts +20 -3
  16. package/scripts/harness.ts +566 -133
  17. package/src/cli/gateway-record.ts +16 -1
  18. package/src/control-plane/agent-realtime-api.ts +4 -0
  19. package/src/control-plane/prompt/agent-prompt-extractor.ts +191 -0
  20. package/src/control-plane/prompt/extractors/claude-prompt-extractor.ts +53 -0
  21. package/src/control-plane/prompt/extractors/codex-prompt-extractor.ts +50 -0
  22. package/src/control-plane/prompt/extractors/cursor-prompt-extractor.ts +56 -0
  23. package/src/control-plane/prompt/session-prompt-engine.ts +69 -0
  24. package/src/control-plane/prompt/thread-title-namer.ts +290 -0
  25. package/src/control-plane/stream-command-parser.ts +12 -0
  26. package/src/control-plane/stream-protocol.ts +109 -0
  27. package/src/control-plane/stream-server-command.ts +14 -0
  28. package/src/control-plane/stream-server-session-runtime.ts +12 -0
  29. package/src/control-plane/stream-server.ts +485 -19
  30. package/src/mux/input-shortcuts.ts +9 -0
  31. package/src/mux/live-mux/critique-review.ts +5 -1
  32. package/src/mux/live-mux/git-parsing.ts +24 -0
  33. package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
  34. package/src/mux/render-frame.ts +1 -1
  35. package/src/pty/pty_host.ts +46 -1
  36. package/src/services/control-plane.ts +22 -0
  37. package/src/services/runtime-control-actions.ts +69 -0
  38. package/src/services/runtime-navigation-input.ts +4 -0
  39. package/src/services/runtime-rail-input.ts +4 -0
  40. package/src/services/runtime-workspace-actions.ts +5 -0
  41. package/src/ui/global-shortcut-input.ts +2 -0
@@ -0,0 +1,65 @@
1
+ import type { AsyncIterableStream } from './types.ts';
2
+
3
+ export function toAsyncIterableStream<T>(stream: ReadableStream<T>): AsyncIterableStream<T> {
4
+ const candidate = stream as AsyncIterableStream<T>;
5
+ if (typeof candidate[Symbol.asyncIterator] === 'function') {
6
+ return candidate;
7
+ }
8
+
9
+ Object.defineProperty(candidate, Symbol.asyncIterator, {
10
+ configurable: true,
11
+ enumerable: false,
12
+ writable: false,
13
+ value: async function* iterator(): AsyncGenerator<T, void, unknown> {
14
+ const reader = stream.getReader();
15
+ try {
16
+ while (true) {
17
+ const { value, done } = await reader.read();
18
+ if (done) {
19
+ break;
20
+ }
21
+ if (value !== undefined) {
22
+ yield value;
23
+ }
24
+ }
25
+ } finally {
26
+ reader.releaseLock();
27
+ }
28
+ },
29
+ });
30
+
31
+ return candidate;
32
+ }
33
+
34
+ export async function consumeReadableStream<T>(stream: ReadableStream<T>): Promise<void> {
35
+ const reader = stream.getReader();
36
+ try {
37
+ while (true) {
38
+ const { done } = await reader.read();
39
+ if (done) {
40
+ break;
41
+ }
42
+ }
43
+ } finally {
44
+ reader.releaseLock();
45
+ }
46
+ }
47
+
48
+ export async function collectReadableStream<T>(stream: ReadableStream<T>): Promise<T[]> {
49
+ const output: T[] = [];
50
+ const reader = stream.getReader();
51
+ try {
52
+ while (true) {
53
+ const { value, done } = await reader.read();
54
+ if (done) {
55
+ break;
56
+ }
57
+ if (value !== undefined) {
58
+ output.push(value);
59
+ }
60
+ }
61
+ } finally {
62
+ reader.releaseLock();
63
+ }
64
+ return output;
65
+ }
@@ -0,0 +1,36 @@
1
+ export { createAnthropic, anthropic, anthropicTools } from './anthropic-provider.ts';
2
+ export { streamText, generateText, collectFullStream } from './stream-text.ts';
3
+ export { streamObject } from './stream-object.ts';
4
+ export {
5
+ createUIMessageStream,
6
+ createUIMessageStreamResponse,
7
+ JsonToSseTransformStream,
8
+ UI_MESSAGE_STREAM_HEADERS,
9
+ } from './ui-stream.ts';
10
+ export type {
11
+ AnthropicProviderToolDefinition,
12
+ AssistantModelMessage,
13
+ AsyncIterableStream,
14
+ FinishReason,
15
+ FunctionToolDefinition,
16
+ GenerateTextOptions,
17
+ GenerateTextResult,
18
+ HarnessAnthropicModel,
19
+ JsonSchema,
20
+ JsonValue,
21
+ LanguageModelResponseMetadata,
22
+ LanguageModelUsage,
23
+ ModelMessage,
24
+ StreamObjectOptions,
25
+ StreamObjectResult,
26
+ StreamTextOptions,
27
+ StreamTextPart,
28
+ StreamTextResult,
29
+ ToolDefinition,
30
+ ToolSet,
31
+ TypedToolCall,
32
+ TypedToolError,
33
+ TypedToolResult,
34
+ UIMessageChunk,
35
+ } from './types.ts';
36
+ export type { AnthropicModelFactory, CreateAnthropicOptions } from './anthropic-provider.ts';
@@ -0,0 +1,66 @@
1
+ export function safeJsonParse(text: string): unknown | undefined {
2
+ try {
3
+ return JSON.parse(text) as unknown;
4
+ } catch {
5
+ return undefined;
6
+ }
7
+ }
8
+
9
+ export function extractFirstBalancedJsonObject(text: string): string | undefined {
10
+ const start = text.indexOf('{');
11
+ if (start < 0) {
12
+ return undefined;
13
+ }
14
+
15
+ let depth = 0;
16
+ let inString = false;
17
+ let escaping = false;
18
+
19
+ for (let index = start; index < text.length; index += 1) {
20
+ const char = text[index];
21
+ if (char === undefined) {
22
+ continue;
23
+ }
24
+
25
+ if (escaping) {
26
+ escaping = false;
27
+ continue;
28
+ }
29
+
30
+ if (char === '\\') {
31
+ escaping = true;
32
+ continue;
33
+ }
34
+
35
+ if (char === '"') {
36
+ inString = !inString;
37
+ continue;
38
+ }
39
+
40
+ if (inString) {
41
+ continue;
42
+ }
43
+
44
+ if (char === '{') {
45
+ depth += 1;
46
+ continue;
47
+ }
48
+
49
+ if (char === '}') {
50
+ depth -= 1;
51
+ if (depth === 0) {
52
+ return text.slice(start, index + 1);
53
+ }
54
+ }
55
+ }
56
+
57
+ return undefined;
58
+ }
59
+
60
+ export function parseJsonObjectFromText(text: string): unknown | undefined {
61
+ const objectSlice = extractFirstBalancedJsonObject(text);
62
+ if (objectSlice === undefined) {
63
+ return undefined;
64
+ }
65
+ return safeJsonParse(objectSlice);
66
+ }
@@ -0,0 +1,80 @@
1
+ interface SseEvent {
2
+ readonly event: string;
3
+ readonly data: string;
4
+ }
5
+
6
+ export function parseSseEventBlock(block: string): SseEvent | null {
7
+ const lines = block.split('\n');
8
+ let eventName = 'message';
9
+ const dataLines: string[] = [];
10
+
11
+ for (const line of lines) {
12
+ if (line.startsWith(':')) {
13
+ continue;
14
+ }
15
+ if (line.startsWith('event:')) {
16
+ eventName = line.slice('event:'.length).trim() || 'message';
17
+ continue;
18
+ }
19
+ if (line.startsWith('data:')) {
20
+ dataLines.push(line.slice('data:'.length).trimStart());
21
+ }
22
+ }
23
+
24
+ if (dataLines.length === 0) {
25
+ return null;
26
+ }
27
+
28
+ return {
29
+ event: eventName,
30
+ data: dataLines.join('\n'),
31
+ };
32
+ }
33
+
34
+ export function createSseEventStream(input: ReadableStream<Uint8Array>): ReadableStream<SseEvent> {
35
+ return new ReadableStream<SseEvent>({
36
+ async start(controller) {
37
+ const decoder = new TextDecoder();
38
+ const reader = input.getReader();
39
+ let buffer = '';
40
+
41
+ try {
42
+ while (true) {
43
+ const { value, done } = await reader.read();
44
+ if (done) {
45
+ break;
46
+ }
47
+ if (value === undefined) {
48
+ continue;
49
+ }
50
+
51
+ buffer += decoder.decode(value, { stream: true });
52
+ while (true) {
53
+ const boundary = buffer.indexOf('\n\n');
54
+ if (boundary < 0) {
55
+ break;
56
+ }
57
+ const block = buffer.slice(0, boundary);
58
+ buffer = buffer.slice(boundary + 2);
59
+ const parsed = parseSseEventBlock(block);
60
+ if (parsed !== null) {
61
+ controller.enqueue(parsed);
62
+ }
63
+ }
64
+ }
65
+
66
+ buffer += decoder.decode();
67
+ const tail = parseSseEventBlock(buffer);
68
+ if (tail !== null) {
69
+ controller.enqueue(tail);
70
+ }
71
+
72
+ controller.close();
73
+ } catch (error) {
74
+ controller.error(error);
75
+ } finally {
76
+ reader.releaseLock();
77
+ }
78
+ },
79
+ });
80
+ }
@@ -0,0 +1,96 @@
1
+ import { toAsyncIterableStream } from './async-iterable-stream.ts';
2
+ import { parseJsonObjectFromText } from './json-parse.ts';
3
+ import { streamText } from './stream-text.ts';
4
+ import type {
5
+ StreamObjectOptions,
6
+ StreamObjectResult,
7
+ StreamTextOptions,
8
+ ToolSet,
9
+ } from './types.ts';
10
+
11
+ function buildJsonInstruction(schema: Record<string, unknown>): string {
12
+ return [
13
+ 'Respond with strict JSON only.',
14
+ 'Do not include markdown fences or extra commentary.',
15
+ `JSON schema: ${JSON.stringify(schema)}`,
16
+ ].join(' ');
17
+ }
18
+
19
+ function withObjectInstruction<T, TOOLS extends ToolSet>(
20
+ options: StreamObjectOptions<T, TOOLS>,
21
+ ): StreamTextOptions<TOOLS> {
22
+ const instruction = buildJsonInstruction(options.schema);
23
+
24
+ if (options.prompt !== undefined) {
25
+ return {
26
+ ...options,
27
+ prompt: `${options.prompt}\n\n${instruction}`,
28
+ };
29
+ }
30
+
31
+ const messages = options.messages ?? [];
32
+ return {
33
+ ...options,
34
+ messages,
35
+ system: options.system !== undefined ? `${options.system}\n\n${instruction}` : instruction,
36
+ };
37
+ }
38
+
39
+ export function streamObject<T, TOOLS extends ToolSet>(
40
+ options: StreamObjectOptions<T, TOOLS>,
41
+ ): StreamObjectResult<T> {
42
+ const result = streamText(withObjectInstruction(options));
43
+
44
+ let assembledText = '';
45
+ let lastJsonSnapshot = '';
46
+
47
+ const partialObjectStream = toAsyncIterableStream(
48
+ result.fullStream.pipeThrough(
49
+ new TransformStream({
50
+ transform(part, controller) {
51
+ if (part.type !== 'text-delta') {
52
+ return;
53
+ }
54
+
55
+ assembledText += part.text;
56
+ const maybeObject = parseJsonObjectFromText(assembledText);
57
+ if (
58
+ maybeObject === undefined ||
59
+ typeof maybeObject !== 'object' ||
60
+ maybeObject === null
61
+ ) {
62
+ return;
63
+ }
64
+
65
+ const serialized = JSON.stringify(maybeObject);
66
+ if (serialized === lastJsonSnapshot) {
67
+ return;
68
+ }
69
+
70
+ lastJsonSnapshot = serialized;
71
+ controller.enqueue(maybeObject as Partial<T>);
72
+ },
73
+ }),
74
+ ),
75
+ );
76
+
77
+ const object = result.text.then((text) => {
78
+ const parsed = parseJsonObjectFromText(text);
79
+ if (parsed === undefined) {
80
+ throw new Error('streamObject failed: no JSON object found in model output');
81
+ }
82
+
83
+ if (options.validate !== undefined && !options.validate(parsed)) {
84
+ throw new Error('streamObject failed: parsed JSON did not pass validator');
85
+ }
86
+
87
+ return parsed as T;
88
+ });
89
+
90
+ return {
91
+ partialObjectStream,
92
+ object,
93
+ text: result.text,
94
+ finishReason: result.finishReason,
95
+ };
96
+ }