@softov/ahpc 0.1.0 → 0.2.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 +40 -1
- package/dist/src/ahp/channels.js +10 -3
- package/dist/src/ahp/publish.js +31 -6
- package/dist/src/cli/main.d.ts +1 -1
- package/dist/src/cli/main.js +70 -98
- package/dist/src/flags.js +4 -0
- package/dist/src/mcp/http.d.ts +24 -0
- package/dist/src/mcp/http.js +140 -0
- package/dist/src/mcp/serve.d.ts +58 -0
- package/dist/src/mcp/serve.js +118 -0
- package/dist/src/mcp/stdio.d.ts +13 -0
- package/dist/src/mcp/stdio.js +58 -0
- package/dist/src/mcp/tools.d.ts +33 -0
- package/dist/src/mcp/tools.js +328 -0
- package/dist/src/wait.d.ts +45 -0
- package/dist/src/wait.js +147 -0
- package/package.json +1 -1
package/dist/src/wait.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Watching one session until it does something, for whoever is asking.
|
|
3
|
+
*
|
|
4
|
+
* The CLI and the tool server both say a thing and then block until the turn
|
|
5
|
+
* it started has finished, and both need the same answer to "finished" - which
|
|
6
|
+
* is not "the last turn in the list" and not "nothing is running". Written
|
|
7
|
+
* once here, because the two disagreeing would mean a tool that returned
|
|
8
|
+
* before the reply was complete while the same command in a shell waited.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Watch one session until it does something, then stop watching.
|
|
12
|
+
*
|
|
13
|
+
* Every streaming command is this with a different stopping condition. The
|
|
14
|
+
* subscription is always closed - a caller that left one open would be a
|
|
15
|
+
* process that never exits, which is the one thing a shell cannot work around.
|
|
16
|
+
*/
|
|
17
|
+
export function until(host, uri, done, options = {}) {
|
|
18
|
+
return new Promise((answer) => {
|
|
19
|
+
let closed = false;
|
|
20
|
+
/*
|
|
21
|
+
* The handle may not exist yet when this runs.
|
|
22
|
+
*
|
|
23
|
+
* A host is entitled to deliver the opening snapshot *synchronously*
|
|
24
|
+
* inside `subscribe` - the scripted one does, and it is the honest thing
|
|
25
|
+
* for a host holding the state already - so a condition satisfied by that
|
|
26
|
+
* first event fires before `subscribe` has returned anything to close.
|
|
27
|
+
* Reading the handle there threw, which made every waiting command fail
|
|
28
|
+
* against the scripted host and work against a socket, purely because one
|
|
29
|
+
* of them answers a tick later.
|
|
30
|
+
*/
|
|
31
|
+
let handle;
|
|
32
|
+
const finish = (event) => {
|
|
33
|
+
if (closed)
|
|
34
|
+
return;
|
|
35
|
+
closed = true;
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
handle?.close();
|
|
38
|
+
answer(event);
|
|
39
|
+
};
|
|
40
|
+
const timer = setTimeout(() => finish(undefined), Math.max(1, (options.timeoutSeconds ?? 900)) * 1000);
|
|
41
|
+
timer.unref?.();
|
|
42
|
+
handle = host.subscribe(uri, (event) => {
|
|
43
|
+
options.onEvent?.(event);
|
|
44
|
+
if (done(event))
|
|
45
|
+
finish(event);
|
|
46
|
+
});
|
|
47
|
+
// Already over, before there was a handle to close. Closing it now is what
|
|
48
|
+
// `finish` could not do.
|
|
49
|
+
if (closed)
|
|
50
|
+
handle.close();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** A turn, as a line of prose rather than a tree of parts. */
|
|
54
|
+
export const spoken = (turn) => turn.parts
|
|
55
|
+
.map((part) => (part.kind === 'markdown' ? part.content : ''))
|
|
56
|
+
.join('')
|
|
57
|
+
.trim();
|
|
58
|
+
/**
|
|
59
|
+
* Say something, and block until the turn it starts has finished.
|
|
60
|
+
*
|
|
61
|
+
* Subscribed before saying anything: the first snapshot is the baseline that
|
|
62
|
+
* says which turns were already there, and one taken afterwards would count
|
|
63
|
+
* the new turn among them.
|
|
64
|
+
*
|
|
65
|
+
* "Finished" is a turn of *ours* having ended - one that was not in the
|
|
66
|
+
* baseline - rather than the last in the list, which is a different session's
|
|
67
|
+
* answer when two people are talking in the same chat. A turn that stops to
|
|
68
|
+
* ask a person something is not finished and is not this caller's to answer,
|
|
69
|
+
* so the wait runs on until whoever is answering has.
|
|
70
|
+
*
|
|
71
|
+
* Answers `undefined` where the timeout ran out, which is a caller's to
|
|
72
|
+
* report rather than to throw: the turn is still going and the session is
|
|
73
|
+
* still there.
|
|
74
|
+
*/
|
|
75
|
+
export async function turn(host, uri, text, options = {}) {
|
|
76
|
+
let given = 0;
|
|
77
|
+
let sawActive = false;
|
|
78
|
+
let before = new Set();
|
|
79
|
+
let first = true;
|
|
80
|
+
let noted;
|
|
81
|
+
let answer;
|
|
82
|
+
const finished = until(host, uri, (event) => {
|
|
83
|
+
/*
|
|
84
|
+
* Two vocabularies, because a host may speak either.
|
|
85
|
+
*
|
|
86
|
+
* `live.ts` rebuilds the whole view and sends a `snapshot` after every
|
|
87
|
+
* action; `fake.ts` sends what changed - `turnStarted`, `delta`,
|
|
88
|
+
* `turnComplete`. Both are `HostEvent` and both are legal, and a caller
|
|
89
|
+
* that read only snapshots waited for ever on the scripted host, which
|
|
90
|
+
* this client offers as the one that needs nothing installed.
|
|
91
|
+
*/
|
|
92
|
+
if (event.type === 'turnStarted') {
|
|
93
|
+
sawActive = true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (event.type === 'delta') {
|
|
97
|
+
if (event.kind === 'markdown')
|
|
98
|
+
options.onDelta?.(event.text);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (event.type === 'toolCall' && event.call.status === 'pending-confirmation') {
|
|
102
|
+
if (noted !== event.call.id) {
|
|
103
|
+
noted = event.call.id;
|
|
104
|
+
options.onWaiting?.({ id: event.call.id, name: event.call.name });
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (event.type === 'turnComplete') {
|
|
109
|
+
if (before.has(event.turn.id))
|
|
110
|
+
return false;
|
|
111
|
+
answer = event.turn;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (event.type !== 'snapshot')
|
|
115
|
+
return false;
|
|
116
|
+
if (first) {
|
|
117
|
+
first = false;
|
|
118
|
+
before = new Set(event.turns.map((one) => one.id));
|
|
119
|
+
}
|
|
120
|
+
if (event.active) {
|
|
121
|
+
sawActive = true;
|
|
122
|
+
const now = spoken(event.active);
|
|
123
|
+
if (now.length > given) {
|
|
124
|
+
options.onDelta?.(now.slice(given));
|
|
125
|
+
given = now.length;
|
|
126
|
+
}
|
|
127
|
+
const call = event.active.parts.find((part) => part.kind === 'toolCall'
|
|
128
|
+
&& part.call.status === 'pending-confirmation');
|
|
129
|
+
if (call?.kind === 'toolCall' && noted !== call.call.id) {
|
|
130
|
+
noted = call.call.id;
|
|
131
|
+
options.onWaiting?.({ id: call.call.id, name: call.call.name });
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
// Something wants a person. Not finished, and not this caller's to answer.
|
|
136
|
+
if (event.input)
|
|
137
|
+
return false;
|
|
138
|
+
const fresh = event.turns.filter((one) => one.role === 'agent' && !before.has(one.id));
|
|
139
|
+
if (!sawActive && fresh.length === 0)
|
|
140
|
+
return false;
|
|
141
|
+
answer = fresh[fresh.length - 1];
|
|
142
|
+
return true;
|
|
143
|
+
}, { ...(options.timeoutSeconds === undefined ? {} : { timeoutSeconds: options.timeoutSeconds }) });
|
|
144
|
+
host.say(uri, text, options.model);
|
|
145
|
+
await finished;
|
|
146
|
+
return answer;
|
|
147
|
+
}
|