@agentshouse/kit 0.2.1-alpha.19 → 0.2.1-alpha.20
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 +4 -0
- package/dist/abort.js +9 -0
- package/dist/bootstrap.json +5 -5
- package/dist/clis.js +8 -7
- package/dist/conversations.js +40 -17
- package/dist/transcription.js +118 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -48,6 +48,10 @@ workspace. Commit in one and run `house git push --owner` there to send the
|
|
|
48
48
|
commit to House; in the container, run it through the install command from the
|
|
49
49
|
copy's directory. Inside an Agent conversation the Agent runs `house git push`.
|
|
50
50
|
|
|
51
|
+
House Kit turns each audio file of your message to an Agent into text on this
|
|
52
|
+
computer before the Agent reads the message. The first audio file downloads the
|
|
53
|
+
speech engine and model, about 425 MB, into `~/.house-kit`.
|
|
54
|
+
|
|
51
55
|
## License
|
|
52
56
|
|
|
53
57
|
MIT
|
package/dist/abort.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function unlessAborted(work, signal) {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const aborted = () => reject(signal.reason);
|
|
4
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
5
|
+
if (signal.aborted)
|
|
6
|
+
aborted();
|
|
7
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', aborted));
|
|
8
|
+
});
|
|
9
|
+
}
|
package/dist/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.2.1-alpha.
|
|
3
|
+
"version": "0.2.1-alpha.20",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.20/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.20/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"windows": {
|
|
20
|
-
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.
|
|
20
|
+
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.20/connect-windows.ps1)))",
|
|
21
21
|
"asset": {
|
|
22
22
|
"name": "connect-windows.ps1",
|
|
23
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.
|
|
23
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.20/connect-windows.ps1"
|
|
24
24
|
},
|
|
25
25
|
"workspace": {
|
|
26
26
|
"argument": "--workspace",
|
package/dist/clis.js
CHANGED
|
@@ -111,28 +111,29 @@ async function installedAdapter(kind) {
|
|
|
111
111
|
export function withoutProxy() {
|
|
112
112
|
return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
|
|
113
113
|
}
|
|
114
|
-
export function run(command, args, timeoutMs, detached = false) {
|
|
114
|
+
export function run(command, args, timeoutMs, detached = false, signal) {
|
|
115
115
|
return new Promise((resolve) => {
|
|
116
116
|
const child = spawn(command, args, {
|
|
117
117
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
118
118
|
timeout: timeoutMs,
|
|
119
119
|
env: withoutProxy(),
|
|
120
120
|
detached,
|
|
121
|
+
signal,
|
|
121
122
|
});
|
|
122
123
|
let stdout = '';
|
|
123
124
|
let output = '';
|
|
124
|
-
child.stdout.on('data', (chunk) => {
|
|
125
|
-
stdout += chunk
|
|
126
|
-
output += chunk
|
|
125
|
+
child.stdout.setEncoding('utf8').on('data', (chunk) => {
|
|
126
|
+
stdout += chunk;
|
|
127
|
+
output += chunk;
|
|
127
128
|
});
|
|
128
|
-
child.stderr.on('data', (chunk) => {
|
|
129
|
-
output += chunk
|
|
129
|
+
child.stderr.setEncoding('utf8').on('data', (chunk) => {
|
|
130
|
+
output += chunk;
|
|
130
131
|
});
|
|
131
132
|
child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
|
|
132
133
|
child.on('close', (status) => resolve({ status, stdout, output }));
|
|
133
134
|
});
|
|
134
135
|
}
|
|
135
|
-
function failureOf(ran, program) {
|
|
136
|
+
export function failureOf(ran, program) {
|
|
136
137
|
return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
|
|
137
138
|
}
|
|
138
139
|
export async function installAdapter(kind) {
|
package/dist/conversations.js
CHANGED
|
@@ -2,6 +2,7 @@ import { client, } from '@agentclientprotocol/sdk';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { unlessAborted } from './abort.js';
|
|
5
6
|
import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
|
|
6
7
|
import { blocksOf, firstChange } from './blocks.js';
|
|
7
8
|
import { openBridge } from './bridge.js';
|
|
@@ -11,6 +12,7 @@ import { agentBase } from './home.js';
|
|
|
11
12
|
import { instructions } from './instructions.js';
|
|
12
13
|
import { holdSecretInput } from './secret-input.js';
|
|
13
14
|
import { createHowWeWork } from './skills.js';
|
|
15
|
+
import { transcribe } from './transcription.js';
|
|
14
16
|
const KILLED = 'the conversation was killed';
|
|
15
17
|
function causeOf(error) {
|
|
16
18
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -21,15 +23,6 @@ function answerMessage(request, response) {
|
|
|
21
23
|
function logged(error) {
|
|
22
24
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
23
25
|
}
|
|
24
|
-
function unlessKilled(work, signal) {
|
|
25
|
-
return new Promise((resolve, reject) => {
|
|
26
|
-
const killed = () => reject(signal.reason);
|
|
27
|
-
signal.addEventListener('abort', killed, { once: true });
|
|
28
|
-
if (signal.aborted)
|
|
29
|
-
killed();
|
|
30
|
-
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
26
|
function stop(bridge) {
|
|
34
27
|
killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
|
|
35
28
|
}
|
|
@@ -60,6 +53,8 @@ class Conversation {
|
|
|
60
53
|
opening = null;
|
|
61
54
|
kills = 0;
|
|
62
55
|
killed = new AbortController();
|
|
56
|
+
interrupts = 0;
|
|
57
|
+
interrupted = new AbortController();
|
|
63
58
|
open = 0;
|
|
64
59
|
queue = Promise.resolve();
|
|
65
60
|
reports = Promise.resolve();
|
|
@@ -95,6 +90,10 @@ export class Conversations {
|
|
|
95
90
|
conversation.killed.abort(new Error(KILLED));
|
|
96
91
|
this.halt(conversation);
|
|
97
92
|
}
|
|
93
|
+
if (input.kind === 'interrupt') {
|
|
94
|
+
conversation.interrupts++;
|
|
95
|
+
conversation.interrupted.abort();
|
|
96
|
+
}
|
|
98
97
|
const work = conversation.queue.then(() => this.carry(conversation, input));
|
|
99
98
|
conversation.queue = work.catch(() => undefined);
|
|
100
99
|
this.acks.set(input.input_id, work);
|
|
@@ -138,8 +137,11 @@ export class Conversations {
|
|
|
138
137
|
return this.open(conversation, input);
|
|
139
138
|
if (input.kind === 'message')
|
|
140
139
|
return this.message(conversation, input);
|
|
141
|
-
if (input.kind === 'interrupt')
|
|
140
|
+
if (input.kind === 'interrupt') {
|
|
141
|
+
if (--conversation.interrupts === 0)
|
|
142
|
+
conversation.interrupted = new AbortController();
|
|
142
143
|
await this.interrupt(conversation, String(input.turn_id));
|
|
144
|
+
}
|
|
143
145
|
if (input.kind === 'kill') {
|
|
144
146
|
await this.kill(conversation);
|
|
145
147
|
if (--conversation.kills === 0)
|
|
@@ -171,7 +173,9 @@ export class Conversations {
|
|
|
171
173
|
const prompt = [];
|
|
172
174
|
try {
|
|
173
175
|
const route = await this.route(conversation, input.agent_id);
|
|
174
|
-
const
|
|
176
|
+
const files = input.files;
|
|
177
|
+
const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
|
|
178
|
+
const lines = await this.transcribed(conversation, files, paths);
|
|
175
179
|
if (conversation.running === null) {
|
|
176
180
|
const opened = await this.start(conversation, route, input.provider_session_id);
|
|
177
181
|
if (input.provider_session_id === null)
|
|
@@ -184,8 +188,8 @@ export class Conversations {
|
|
|
184
188
|
});
|
|
185
189
|
}
|
|
186
190
|
prompt.push({ type: 'text', text: String(input.text) });
|
|
187
|
-
if (
|
|
188
|
-
prompt.push({ type: 'text', text:
|
|
191
|
+
if (lines.length > 0)
|
|
192
|
+
prompt.push({ type: 'text', text: lines.join('\n') });
|
|
189
193
|
if (conversation.kills > 0)
|
|
190
194
|
throw new Error(KILLED);
|
|
191
195
|
}
|
|
@@ -197,6 +201,25 @@ export class Conversations {
|
|
|
197
201
|
this.prompt(conversation, prompt);
|
|
198
202
|
return ack;
|
|
199
203
|
}
|
|
204
|
+
async transcribed(conversation, files, paths) {
|
|
205
|
+
const lines = [];
|
|
206
|
+
for (const [index, path] of paths.entries()) {
|
|
207
|
+
lines.push(path);
|
|
208
|
+
if (files[index].media_type?.startsWith('audio/') !== true)
|
|
209
|
+
continue;
|
|
210
|
+
try {
|
|
211
|
+
const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
|
|
212
|
+
if (text !== '')
|
|
213
|
+
lines.push(`Transcript: ${text}`);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (conversation.kills > 0)
|
|
217
|
+
throw error;
|
|
218
|
+
logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
200
223
|
async interrupt(conversation, turnId) {
|
|
201
224
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
202
225
|
return;
|
|
@@ -235,9 +258,9 @@ export class Conversations {
|
|
|
235
258
|
}
|
|
236
259
|
async route(conversation, agentId) {
|
|
237
260
|
const signal = conversation.killed.signal;
|
|
238
|
-
await
|
|
261
|
+
await unlessAborted(this.kit.agents.read(), signal);
|
|
239
262
|
if (this.kit.agents.route(agentId) === undefined)
|
|
240
|
-
await
|
|
263
|
+
await unlessAborted(this.kit.agents.refresh(), signal);
|
|
241
264
|
const route = this.kit.agents.route(agentId);
|
|
242
265
|
if (route === undefined)
|
|
243
266
|
throw new Error(`this Environment hosts no Agent ${agentId}`);
|
|
@@ -253,7 +276,7 @@ export class Conversations {
|
|
|
253
276
|
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
254
277
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
255
278
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
|
|
256
|
-
const cli = await
|
|
279
|
+
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
257
280
|
const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
258
281
|
if (conversation.kills > 0) {
|
|
259
282
|
bridge.close();
|
|
@@ -261,7 +284,7 @@ export class Conversations {
|
|
|
261
284
|
}
|
|
262
285
|
conversation.opening = bridge;
|
|
263
286
|
try {
|
|
264
|
-
await
|
|
287
|
+
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
265
288
|
const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
|
|
266
289
|
void adapter.exited.then(() => bridge.close());
|
|
267
290
|
const agent = adapter.connection.agent;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { availableParallelism, tmpdir } from 'node:os';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { unlessAborted } from './abort.js';
|
|
8
|
+
import { failureOf, run } from './clis.js';
|
|
9
|
+
import { kitHome } from './home.js';
|
|
10
|
+
const RELEASE = 'https://github.com/agentshouse/kit/releases/download/engine-1';
|
|
11
|
+
export const MODEL = {
|
|
12
|
+
url: 'https://huggingface.co/ggml-org/parakeet-GGUF/resolve/35156454d1a39de06863303dd209fd2bed6ee079/ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
|
|
13
|
+
sha256: '8b205b8b39c6535e153de6fb11c51db46125d45c4f16ba496fe41a0fe71b885e',
|
|
14
|
+
executable: false,
|
|
15
|
+
};
|
|
16
|
+
export const ENGINES = {
|
|
17
|
+
'linux-x64': {
|
|
18
|
+
decoder: { url: `${RELEASE}/ffmpeg-linux-x86_64`, sha256: '1f8235d206428305afb89c6e8ab99d100425a0ba61e22bb4c7aff017db8c8de5', executable: true },
|
|
19
|
+
engine: { url: `${RELEASE}/parakeet-cli-linux-x86_64`, sha256: '259fd7a1c83f9768be45d15ef88965f82ce86c481215cfe34f778f9890b3a833', executable: true },
|
|
20
|
+
},
|
|
21
|
+
'linux-arm64': {
|
|
22
|
+
decoder: { url: `${RELEASE}/ffmpeg-linux-aarch64`, sha256: 'a7e08a0fae0179d1078312f6bb13d2a9aa4d4c71a1d82a022953f25dee452f5e', executable: true },
|
|
23
|
+
engine: { url: `${RELEASE}/parakeet-cli-linux-aarch64`, sha256: 'e2585bc37a30baf48b16e19d18e56920cba78c05da8dc0dea2b0dab0b26caab9', executable: true },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const CHUNK_SECONDS = 20;
|
|
27
|
+
let held = Promise.resolve();
|
|
28
|
+
function directory() {
|
|
29
|
+
return join(kitHome(), 'transcription');
|
|
30
|
+
}
|
|
31
|
+
function installed(artifact) {
|
|
32
|
+
return join(directory(), artifact.sha256);
|
|
33
|
+
}
|
|
34
|
+
async function executed(command, args, signal) {
|
|
35
|
+
const ran = await run(command, args, 0, false, signal);
|
|
36
|
+
if (ran.status !== 0)
|
|
37
|
+
throw new Error(failureOf(ran, basename(command)));
|
|
38
|
+
return ran.stdout;
|
|
39
|
+
}
|
|
40
|
+
function present(path) {
|
|
41
|
+
return stat(path).then(() => true, () => false);
|
|
42
|
+
}
|
|
43
|
+
async function cpus() {
|
|
44
|
+
const [quota, period] = (await readFile('/sys/fs/cgroup/cpu.max', 'utf8').catch(() => 'max')).split(' ');
|
|
45
|
+
const allowed = quota === 'max' ? Infinity : Math.ceil(Number(quota) / Number(period));
|
|
46
|
+
return Math.min(availableParallelism(), allowed);
|
|
47
|
+
}
|
|
48
|
+
async function sha256(path) {
|
|
49
|
+
const hash = createHash('sha256');
|
|
50
|
+
await pipeline(createReadStream(path), hash);
|
|
51
|
+
return hash.digest('hex');
|
|
52
|
+
}
|
|
53
|
+
async function install(artifacts, signal) {
|
|
54
|
+
await mkdir(directory(), { recursive: true });
|
|
55
|
+
const pinned = new Set(artifacts.map((artifact) => artifact.sha256));
|
|
56
|
+
for (const name of await readdir(directory())) {
|
|
57
|
+
if (!pinned.has(name))
|
|
58
|
+
await rm(join(directory(), name), { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
const missing = [];
|
|
61
|
+
for (const artifact of artifacts)
|
|
62
|
+
if (!(await present(installed(artifact))))
|
|
63
|
+
missing.push(artifact);
|
|
64
|
+
try {
|
|
65
|
+
for (const artifact of missing) {
|
|
66
|
+
const part = `${installed(artifact)}.part`;
|
|
67
|
+
await executed('curl', ['-fsSL', '--retry', '3', '-o', part, artifact.url], signal);
|
|
68
|
+
if ((await sha256(part)) !== artifact.sha256)
|
|
69
|
+
throw new Error(`${artifact.url} does not match its sha256`);
|
|
70
|
+
}
|
|
71
|
+
for (const artifact of missing) {
|
|
72
|
+
if (artifact.executable)
|
|
73
|
+
await chmod(`${installed(artifact)}.part`, 0o755);
|
|
74
|
+
await rename(`${installed(artifact)}.part`, installed(artifact));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
for (const artifact of missing)
|
|
79
|
+
await rm(`${installed(artifact)}.part`, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function transcribed(path, signal) {
|
|
83
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
84
|
+
const pinned = ENGINES[platform];
|
|
85
|
+
if (pinned === undefined)
|
|
86
|
+
throw new Error(`no transcription engine is pinned for ${platform}`);
|
|
87
|
+
await install([pinned.decoder, pinned.engine, MODEL], signal);
|
|
88
|
+
const work = await mkdtemp(join(tmpdir(), 'kit-transcription-'));
|
|
89
|
+
try {
|
|
90
|
+
await executed(installed(pinned.decoder), ['-nostdin', '-hide_banner', '-loglevel', 'error', '-i', path, '-ar', '16000', '-ac', '1', '-c:a', 'pcm_s16le',
|
|
91
|
+
'-f', 'segment', '-segment_time', String(CHUNK_SECONDS), join(work, '%05d.wav')], signal);
|
|
92
|
+
const parts = (await readdir(work)).sort().flatMap((part) => ['-f', join(work, part)]);
|
|
93
|
+
const text = await executed(installed(pinned.engine), ['-t', String(await cpus()), '-np', '-m', installed(MODEL), ...parts], signal);
|
|
94
|
+
return text
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((line) => line.trim())
|
|
97
|
+
.filter((line) => line !== '')
|
|
98
|
+
.join(' ');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await rm(work, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export async function transcribe(path, signal) {
|
|
105
|
+
const before = held;
|
|
106
|
+
let release;
|
|
107
|
+
const mine = new Promise((resolve) => {
|
|
108
|
+
release = resolve;
|
|
109
|
+
});
|
|
110
|
+
held = Promise.all([before, mine]);
|
|
111
|
+
try {
|
|
112
|
+
await unlessAborted(before, signal);
|
|
113
|
+
return await transcribed(path, signal);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
release();
|
|
117
|
+
}
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/kit",
|
|
3
|
-
"version": "0.2.1-alpha.
|
|
3
|
+
"version": "0.2.1-alpha.20",
|
|
4
4
|
"description": "House Kit connects an Environment to agents.house.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc -p tsconfig.build.json && node scripts/bootstrap-manifest.mjs",
|
|
25
|
+
"engine": "mkdir -p tmp/engine && docker run --rm -v \"$PWD:/kit\" -w /kit alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 sh scripts/engine.sh tmp/engine",
|
|
25
26
|
"test": "tsc -p tsconfig.json && vitest run"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|