@crouton-kit/humanloop 0.1.3 → 0.2.1

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/dist/api.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { Deck, ResolutionEnvelope, GenerateVisual } from './types.js';
2
+ export interface AskOpts {
3
+ /** Interaction directory. Defaults to a managed temp dir under os.tmpdir(). */
4
+ dir?: string;
5
+ sessionId?: string;
6
+ cols?: number;
7
+ rows?: number;
8
+ }
9
+ /**
10
+ * Resolve a deck against an interaction directory and return the resolution
11
+ * envelope. Writes `<dir>/deck.json` (the request, per the convention) and,
12
+ * on completion, `<dir>/response.json`.
13
+ */
14
+ export declare function ask(deck: Deck, opts?: AskOpts): Promise<ResolutionEnvelope>;
15
+ export interface ApproveOpts {
16
+ subtitle?: string;
17
+ body?: string;
18
+ dir?: string;
19
+ sessionId?: string;
20
+ }
21
+ /** Sugar: a single `kind:'validation'` Yes/No interaction. */
22
+ export declare function approve(title: string, opts?: ApproveOpts): Promise<boolean>;
23
+ /** Sugar: a single `kind:'notify'` acknowledgement. */
24
+ export declare function notify(title: string, body?: string): Promise<void>;
25
+ export interface InboxOpts {
26
+ cols?: number;
27
+ rows?: number;
28
+ generateVisual?: GenerateVisual;
29
+ }
30
+ /**
31
+ * List → resolve loop across `roots`. Shows pending interactions, lets the
32
+ * human pick one, resolves it (writing its `response.json`), then rescans —
33
+ * resolved items drop out — until the human quits or nothing is pending.
34
+ */
35
+ export declare function inbox(roots: string[], opts?: InboxOpts): Promise<void>;
package/dist/api.js ADDED
@@ -0,0 +1,119 @@
1
+ import { mkdtempSync, mkdirSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { resolveInteractionDir } from './tui/app.js';
5
+ import { scanInbox } from './inbox/scan.js';
6
+ import { pickFromInbox } from './inbox/tui.js';
7
+ import { deckPath, atomicWriteJson, readJson } from './inbox/convention.js';
8
+ import { getTerminalSize } from './tui/terminal.js';
9
+ const RESPONSE_SCHEMA_ID = 'humanloop.response/v2';
10
+ function managedDir() {
11
+ return mkdtempSync(join(tmpdir(), 'hl-ix-'));
12
+ }
13
+ /**
14
+ * Deterministic, no-LLM resolution summary — one line per answered
15
+ * interaction: `"<title>: <option label>[ — <freetext>]"`.
16
+ */
17
+ function buildSummary(deck, responses) {
18
+ const byId = new Map(responses.map((r) => [r.id, r]));
19
+ const lines = [];
20
+ for (const it of deck.interactions) {
21
+ const r = byId.get(it.id);
22
+ if (r === undefined)
23
+ continue;
24
+ const opt = r.selectedOptionId !== undefined
25
+ ? it.options.find((o) => o.id === r.selectedOptionId)
26
+ : undefined;
27
+ const ft = r.freetext !== undefined && r.freetext !== '' ? r.freetext : undefined;
28
+ let val;
29
+ if (opt !== undefined && ft !== undefined)
30
+ val = `${opt.label} — ${ft}`;
31
+ else if (opt !== undefined)
32
+ val = opt.label;
33
+ else if (ft !== undefined)
34
+ val = ft;
35
+ else
36
+ val = '(skipped)';
37
+ lines.push(`${it.title}: ${val}`);
38
+ }
39
+ return lines.join('\n');
40
+ }
41
+ /**
42
+ * Resolve a deck against an interaction directory and return the resolution
43
+ * envelope. Writes `<dir>/deck.json` (the request, per the convention) and,
44
+ * on completion, `<dir>/response.json`.
45
+ */
46
+ export async function ask(deck, opts = {}) {
47
+ const dir = opts.dir ?? managedDir();
48
+ mkdirSync(dir, { recursive: true });
49
+ atomicWriteJson(deckPath(dir), deck);
50
+ const { responses, completedAt, responsePath } = await resolveInteractionDir(dir, deck, {
51
+ sessionId: opts.sessionId,
52
+ cols: opts.cols,
53
+ rows: opts.rows,
54
+ });
55
+ return {
56
+ summary: buildSummary(deck, responses),
57
+ responsePath,
58
+ schema: RESPONSE_SCHEMA_ID,
59
+ responses,
60
+ completedAt,
61
+ };
62
+ }
63
+ /** Sugar: a single `kind:'validation'` Yes/No interaction. */
64
+ export async function approve(title, opts = {}) {
65
+ const deck = {
66
+ interactions: [{
67
+ id: 'approve',
68
+ title,
69
+ ...(opts.subtitle !== undefined ? { subtitle: opts.subtitle } : {}),
70
+ ...(opts.body !== undefined ? { body: opts.body } : {}),
71
+ kind: 'validation',
72
+ options: [
73
+ { id: 'yes', label: 'Yes' },
74
+ { id: 'no', label: 'No' },
75
+ ],
76
+ }],
77
+ };
78
+ const env = await ask(deck, { dir: opts.dir, sessionId: opts.sessionId });
79
+ return env.responses[0]?.selectedOptionId === 'yes';
80
+ }
81
+ /** Sugar: a single `kind:'notify'` acknowledgement. */
82
+ export async function notify(title, body) {
83
+ const deck = {
84
+ interactions: [{
85
+ id: 'notify',
86
+ title,
87
+ ...(body !== undefined ? { body } : {}),
88
+ kind: 'notify',
89
+ options: [{ id: 'ok', label: 'OK' }],
90
+ }],
91
+ };
92
+ await ask(deck, {});
93
+ }
94
+ /**
95
+ * List → resolve loop across `roots`. Shows pending interactions, lets the
96
+ * human pick one, resolves it (writing its `response.json`), then rescans —
97
+ * resolved items drop out — until the human quits or nothing is pending.
98
+ */
99
+ export async function inbox(roots, opts = {}) {
100
+ for (;;) {
101
+ const items = scanInbox(roots);
102
+ if (items.length === 0)
103
+ return;
104
+ const term = getTerminalSize();
105
+ const cols = opts.cols ?? term.cols;
106
+ const rows = opts.rows ?? term.rows;
107
+ const picked = await pickFromInbox(items, { cols, rows });
108
+ if (picked === null)
109
+ return;
110
+ const deck = readJson(deckPath(picked.dir));
111
+ if (deck === null)
112
+ continue; // raced/removed — rescan
113
+ await resolveInteractionDir(picked.dir, deck, {
114
+ generateVisual: opts.generateVisual,
115
+ cols,
116
+ rows,
117
+ });
118
+ }
119
+ }