@agent-link/agent 0.1.326 → 0.1.328
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/backends/types.d.ts +8 -0
- package/dist/backends/types.js +15 -0
- package/dist/backends/types.js.map +1 -1
- package/dist/connection.js +66 -22
- package/dist/connection.js.map +1 -1
- package/dist/pi.d.ts +4 -0
- package/dist/pi.js +234 -4
- package/dist/pi.js.map +1 -1
- package/dist/provider-command-discovery.d.ts +25 -0
- package/dist/provider-command-discovery.js +42 -0
- package/dist/provider-command-discovery.js.map +1 -0
- package/dist/provider-command-types.d.ts +23 -0
- package/dist/provider-command-types.js +13 -0
- package/dist/provider-command-types.js.map +1 -0
- package/package.json +1 -1
package/dist/pi.js
CHANGED
|
@@ -14,6 +14,7 @@ import { piCapabilities, } from './backends/types.js';
|
|
|
14
14
|
import { CONFIG_DIR } from './config.js';
|
|
15
15
|
import { getCleanEnv, resetCachedExecutablePath, resolveWindowsNpmWrapper, } from './sdk.js';
|
|
16
16
|
import { getProvider, resolveProviderCommand } from './providers.js';
|
|
17
|
+
import { providerCommandsMessage, } from './provider-command-types.js';
|
|
17
18
|
const conversations = new Map();
|
|
18
19
|
const sendFns = new Set();
|
|
19
20
|
const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
@@ -23,8 +24,15 @@ const MIME_PATTERN = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i
|
|
|
23
24
|
const IMAGE_MIME_PATTERN = /^image\/(?:png|jpeg|gif|webp)$/i;
|
|
24
25
|
const TURN_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
25
26
|
const RPC_TIMEOUT_MS = 30_000;
|
|
27
|
+
const DISCOVERY_TIMEOUT_MS = 15_000;
|
|
26
28
|
const ABORT_TIMEOUT_MS = 5_000;
|
|
27
29
|
const MAX_STDERR_CHARS = 64 * 1024;
|
|
30
|
+
const MAX_DISCOVERY_STDOUT_CHARS = 1024 * 1024;
|
|
31
|
+
const MAX_COMMAND_NAME_CHARS = 128;
|
|
32
|
+
const MAX_COMMAND_DESCRIPTION_CHARS = 4096;
|
|
33
|
+
const MAX_COMMAND_METADATA_CHARS = 4096;
|
|
34
|
+
const PI_COMMAND_SOURCES = new Set(['skill', 'prompt', 'extension']);
|
|
35
|
+
const PI_COMMAND_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
28
36
|
const MAX_ATTACHMENTS = 5;
|
|
29
37
|
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
30
38
|
const PI_SESSION_LIST_LIMIT = 150;
|
|
@@ -69,6 +77,8 @@ function createState(conversationId, workDir, providerId = 'pi') {
|
|
|
69
77
|
model: null,
|
|
70
78
|
thinking: null,
|
|
71
79
|
availableModels: [],
|
|
80
|
+
commands: [],
|
|
81
|
+
commandCatalogGeneration: 0,
|
|
72
82
|
sessionNotified: false,
|
|
73
83
|
cancelRequested: false,
|
|
74
84
|
protocolFailed: false,
|
|
@@ -103,6 +113,11 @@ export function rebindPiConversation(piSessionId, newConversationId, providerId
|
|
|
103
113
|
conversations.delete(oldConversationId);
|
|
104
114
|
state.conversationId = newConversationId;
|
|
105
115
|
conversations.set(newConversationId, state);
|
|
116
|
+
if (state.child) {
|
|
117
|
+
const child = state.child;
|
|
118
|
+
invalidatePiCommands(state);
|
|
119
|
+
void refreshPiCommands(state, child, state.commandCatalogGeneration);
|
|
120
|
+
}
|
|
106
121
|
return true;
|
|
107
122
|
}
|
|
108
123
|
}
|
|
@@ -244,10 +259,20 @@ function rejectPendingRpc(state, error) {
|
|
|
244
259
|
}
|
|
245
260
|
state.pendingRpc.clear();
|
|
246
261
|
}
|
|
262
|
+
function invalidatePiCommands(state) {
|
|
263
|
+
state.commandCatalogGeneration++;
|
|
264
|
+
state.commands = [];
|
|
265
|
+
sendFn(providerCommandsMessage({
|
|
266
|
+
providerId: state.providerId,
|
|
267
|
+
conversationId: state.conversationId,
|
|
268
|
+
workDir: state.workDir,
|
|
269
|
+
}, { commands: [] }));
|
|
270
|
+
}
|
|
247
271
|
function terminatePiChild(state) {
|
|
248
272
|
const child = state.child;
|
|
249
273
|
state.child = null;
|
|
250
274
|
state.childProviderId = null;
|
|
275
|
+
invalidatePiCommands(state);
|
|
251
276
|
rejectPendingRpc(state, new Error('PI RPC process was terminated'));
|
|
252
277
|
if (!child)
|
|
253
278
|
return;
|
|
@@ -324,16 +349,29 @@ function writeRpcRecord(state, record) {
|
|
|
324
349
|
function handleRpcResponse(state, record) {
|
|
325
350
|
const id = record.id;
|
|
326
351
|
const command = record.command;
|
|
352
|
+
const pendingById = typeof id === 'string' ? state.pendingRpc.get(id) : undefined;
|
|
327
353
|
if (typeof id !== 'string' || typeof command !== 'string' || typeof record.success !== 'boolean') {
|
|
354
|
+
if (pendingById?.command === 'get_commands') {
|
|
355
|
+
clearTimeout(pendingById.timer);
|
|
356
|
+
state.pendingRpc.delete(id);
|
|
357
|
+
pendingById.reject(new Error('PI RPC get_commands returned a malformed response'));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
328
360
|
protocolFailure(state, 'malformed response record');
|
|
329
361
|
return;
|
|
330
362
|
}
|
|
331
|
-
const pending =
|
|
363
|
+
const pending = pendingById;
|
|
332
364
|
if (!pending) {
|
|
333
365
|
protocolFailure(state, `unexpected response id ${id}`);
|
|
334
366
|
return;
|
|
335
367
|
}
|
|
336
368
|
if (pending.command !== command) {
|
|
369
|
+
if (pending.command === 'get_commands') {
|
|
370
|
+
clearTimeout(pending.timer);
|
|
371
|
+
state.pendingRpc.delete(id);
|
|
372
|
+
pending.reject(new Error(`PI RPC get_commands response command mismatch for ${id}`));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
337
375
|
protocolFailure(state, `response command mismatch for ${id}`);
|
|
338
376
|
return;
|
|
339
377
|
}
|
|
@@ -610,6 +648,84 @@ function attachStrictJsonlReader(state, child) {
|
|
|
610
648
|
protocolFailure(state, 'stdout ended with an unterminated record');
|
|
611
649
|
});
|
|
612
650
|
}
|
|
651
|
+
function normalizePiCommands(response) {
|
|
652
|
+
const data = asRecord(response.data);
|
|
653
|
+
if (!data || !Array.isArray(data.commands)) {
|
|
654
|
+
throw new Error('PI RPC get_commands returned malformed data');
|
|
655
|
+
}
|
|
656
|
+
const commands = [];
|
|
657
|
+
const seen = new Set();
|
|
658
|
+
for (const value of data.commands) {
|
|
659
|
+
const command = asRecord(value);
|
|
660
|
+
if (!command || !PI_COMMAND_SOURCES.has(command.source))
|
|
661
|
+
continue;
|
|
662
|
+
if (typeof command.name !== 'string'
|
|
663
|
+
|| !command.name
|
|
664
|
+
|| command.name.length > MAX_COMMAND_NAME_CHARS
|
|
665
|
+
|| !PI_COMMAND_NAME_PATTERN.test(command.name)
|
|
666
|
+
|| (command.source === 'skill' && !command.name.startsWith('skill:'))
|
|
667
|
+
|| (command.source !== 'skill' && command.name.startsWith('skill:')))
|
|
668
|
+
continue;
|
|
669
|
+
if (command.description != null && typeof command.description !== 'string')
|
|
670
|
+
continue;
|
|
671
|
+
if (command.location != null && typeof command.location !== 'string')
|
|
672
|
+
continue;
|
|
673
|
+
if (command.path != null && typeof command.path !== 'string')
|
|
674
|
+
continue;
|
|
675
|
+
const description = typeof command.description === 'string'
|
|
676
|
+
? command.description.trim().slice(0, MAX_COMMAND_DESCRIPTION_CHARS)
|
|
677
|
+
: '';
|
|
678
|
+
const location = typeof command.location === 'string'
|
|
679
|
+
? command.location.trim().slice(0, MAX_COMMAND_METADATA_CHARS)
|
|
680
|
+
: '';
|
|
681
|
+
const sourcePath = typeof command.path === 'string'
|
|
682
|
+
? command.path.slice(0, MAX_COMMAND_METADATA_CHARS)
|
|
683
|
+
: '';
|
|
684
|
+
const key = `${command.source}\u0000${command.name}`;
|
|
685
|
+
if (seen.has(key))
|
|
686
|
+
continue;
|
|
687
|
+
seen.add(key);
|
|
688
|
+
const source = command.source;
|
|
689
|
+
const name = source === 'skill' ? command.name.slice('skill:'.length) : command.name;
|
|
690
|
+
commands.push({
|
|
691
|
+
id: `${source}:${command.name}`,
|
|
692
|
+
name,
|
|
693
|
+
invocation: `/${command.name}`,
|
|
694
|
+
description,
|
|
695
|
+
kind: source,
|
|
696
|
+
...(location ? { location } : {}),
|
|
697
|
+
...(sourcePath ? { path: sourcePath } : {}),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return commands;
|
|
701
|
+
}
|
|
702
|
+
async function refreshPiCommands(state, child, generation) {
|
|
703
|
+
const scope = {
|
|
704
|
+
providerId: state.providerId,
|
|
705
|
+
conversationId: state.conversationId,
|
|
706
|
+
workDir: resolve(state.workDir),
|
|
707
|
+
};
|
|
708
|
+
try {
|
|
709
|
+
const response = await sendRpcCommand(state, { type: 'get_commands' });
|
|
710
|
+
const commands = normalizePiCommands(response);
|
|
711
|
+
if (state.child !== child
|
|
712
|
+
|| state.commandCatalogGeneration !== generation
|
|
713
|
+
|| state.providerId !== scope.providerId
|
|
714
|
+
|| state.conversationId !== scope.conversationId
|
|
715
|
+
|| normalizePathForCompare(state.workDir) !== normalizePathForCompare(scope.workDir))
|
|
716
|
+
return;
|
|
717
|
+
state.commands = commands;
|
|
718
|
+
sendFn(providerCommandsMessage(scope, { commands }));
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
if (state.child !== child || state.commandCatalogGeneration !== generation)
|
|
722
|
+
return;
|
|
723
|
+
state.commands = [];
|
|
724
|
+
// Command discovery is optional. A valid RPC error or malformed command
|
|
725
|
+
// payload must not poison the persistent chat protocol.
|
|
726
|
+
sendFn(providerCommandsMessage(scope, { commands: [], error: error.message }));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
613
729
|
function directSpawnCommand(command, prefixArgs) {
|
|
614
730
|
const direct = resolveWindowsNpmWrapper(command);
|
|
615
731
|
if (direct) {
|
|
@@ -655,6 +771,9 @@ async function launchPiProcess(state) {
|
|
|
655
771
|
}
|
|
656
772
|
state.child = child;
|
|
657
773
|
state.childProviderId = state.providerId;
|
|
774
|
+
state.commandCatalogGeneration++;
|
|
775
|
+
const commandCatalogGeneration = state.commandCatalogGeneration;
|
|
776
|
+
state.commands = [];
|
|
658
777
|
state.protocolFailed = false;
|
|
659
778
|
state.stderrBuffer = '';
|
|
660
779
|
attachStrictJsonlReader(state, child);
|
|
@@ -666,6 +785,7 @@ async function launchPiProcess(state) {
|
|
|
666
785
|
return;
|
|
667
786
|
state.child = null;
|
|
668
787
|
state.childProviderId = null;
|
|
788
|
+
invalidatePiCommands(state);
|
|
669
789
|
const wrapped = new Error(`Failed to start PI: ${error.message}`);
|
|
670
790
|
rejectPendingRpc(state, wrapped);
|
|
671
791
|
if (state.turnActive)
|
|
@@ -677,6 +797,7 @@ async function launchPiProcess(state) {
|
|
|
677
797
|
return;
|
|
678
798
|
state.child = null;
|
|
679
799
|
state.childProviderId = null;
|
|
800
|
+
invalidatePiCommands(state);
|
|
680
801
|
const diagnostic = state.stderrBuffer.trim();
|
|
681
802
|
const error = new Error(diagnostic || `PI RPC process exited${code == null ? '' : ` with code ${code}`}`);
|
|
682
803
|
rejectPendingRpc(state, error);
|
|
@@ -704,6 +825,7 @@ async function launchPiProcess(state) {
|
|
|
704
825
|
});
|
|
705
826
|
}
|
|
706
827
|
void refreshAvailableModels(state);
|
|
828
|
+
void refreshPiCommands(state, child, commandCatalogGeneration);
|
|
707
829
|
})();
|
|
708
830
|
try {
|
|
709
831
|
await state.launching;
|
|
@@ -739,6 +861,110 @@ async function refreshAvailableModels(state) {
|
|
|
739
861
|
// Model discovery is optional and must not fail an otherwise healthy turn.
|
|
740
862
|
}
|
|
741
863
|
}
|
|
864
|
+
export async function discoverPiCommands(conversationId, workDir, providerId = 'pi') {
|
|
865
|
+
const scope = { providerId, conversationId, workDir: resolve(workDir) };
|
|
866
|
+
let child = null;
|
|
867
|
+
try {
|
|
868
|
+
const provider = getProvider(providerId);
|
|
869
|
+
const resolvedCommand = resolveProviderCommand(provider);
|
|
870
|
+
// Pre-prompt discovery is deliberately ephemeral: it avoids allocating a
|
|
871
|
+
// persisted empty Pi session. Resources are loaded again by the eventual
|
|
872
|
+
// persistent conversation process, which publishes an authoritative refresh.
|
|
873
|
+
const direct = directSpawnCommand(resolvedCommand.command, [...resolvedCommand.prefixArgs, '--mode', 'rpc', '--no-session']);
|
|
874
|
+
child = spawn(direct.command, direct.args, {
|
|
875
|
+
cwd: scope.workDir,
|
|
876
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
877
|
+
env: { ...getCleanEnv(), ...(provider.env || {}) },
|
|
878
|
+
...resolvedCommand.spawnOpts,
|
|
879
|
+
windowsHide: true,
|
|
880
|
+
});
|
|
881
|
+
child.stderr?.on('data', () => { });
|
|
882
|
+
const commandId = `agentlink-discovery-${randomUUID()}`;
|
|
883
|
+
const response = await new Promise((resolveResponse, rejectResponse) => {
|
|
884
|
+
const decoder = new StringDecoder('utf8');
|
|
885
|
+
let pending = '';
|
|
886
|
+
let totalChars = 0;
|
|
887
|
+
let settled = false;
|
|
888
|
+
const finish = (error, responseValue) => {
|
|
889
|
+
if (settled)
|
|
890
|
+
return;
|
|
891
|
+
settled = true;
|
|
892
|
+
clearTimeout(timer);
|
|
893
|
+
if (error)
|
|
894
|
+
rejectResponse(error);
|
|
895
|
+
else
|
|
896
|
+
resolveResponse(responseValue);
|
|
897
|
+
};
|
|
898
|
+
const timer = setTimeout(() => finish(new Error(`PI RPC get_commands timed out after ${DISCOVERY_TIMEOUT_MS}ms`)), DISCOVERY_TIMEOUT_MS);
|
|
899
|
+
timer.unref?.();
|
|
900
|
+
child.on('error', error => finish(new Error(`Failed to start PI discovery: ${error.message}`)));
|
|
901
|
+
child.on('close', code => finish(new Error(`PI discovery process exited${code == null ? '' : ` with code ${code}`}`)));
|
|
902
|
+
child.stdout?.on('data', (chunk) => {
|
|
903
|
+
const text = typeof chunk === 'string' ? chunk : decoder.write(chunk);
|
|
904
|
+
totalChars += text.length;
|
|
905
|
+
if (totalChars > MAX_DISCOVERY_STDOUT_CHARS) {
|
|
906
|
+
finish(new Error('PI discovery output exceeded the protocol limit'));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
pending += text;
|
|
910
|
+
let newlineIndex = pending.indexOf('\n');
|
|
911
|
+
while (!settled && newlineIndex !== -1) {
|
|
912
|
+
const line = pending.slice(0, newlineIndex);
|
|
913
|
+
pending = pending.slice(newlineIndex + 1);
|
|
914
|
+
if (line.trim()) {
|
|
915
|
+
let record;
|
|
916
|
+
try {
|
|
917
|
+
const value = JSON.parse(line);
|
|
918
|
+
const parsed = asRecord(value);
|
|
919
|
+
if (!parsed)
|
|
920
|
+
throw new Error('record is not an object');
|
|
921
|
+
record = parsed;
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
finish(new Error(`PI discovery returned malformed JSONL: ${error.message}`));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (record.type === 'response') {
|
|
928
|
+
if (record.id !== commandId
|
|
929
|
+
|| record.command !== 'get_commands'
|
|
930
|
+
|| typeof record.success !== 'boolean') {
|
|
931
|
+
finish(new Error('PI discovery returned a mismatched response'));
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (!record.success) {
|
|
935
|
+
finish(new Error(typeof record.error === 'string' ? record.error : 'PI RPC get_commands failed'));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
finish(undefined, record);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
newlineIndex = pending.indexOf('\n');
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
child.stdin?.write(`${JSON.stringify({ type: 'get_commands', id: commandId })}\n`, 'utf8', (error) => {
|
|
946
|
+
if (error)
|
|
947
|
+
finish(error);
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
return { commands: normalizePiCommands(response) };
|
|
951
|
+
}
|
|
952
|
+
catch (error) {
|
|
953
|
+
return { commands: [], error: error.message };
|
|
954
|
+
}
|
|
955
|
+
finally {
|
|
956
|
+
if (child) {
|
|
957
|
+
try {
|
|
958
|
+
child.stdin?.end();
|
|
959
|
+
}
|
|
960
|
+
catch { /* already closed */ }
|
|
961
|
+
try {
|
|
962
|
+
child.kill('SIGTERM');
|
|
963
|
+
}
|
|
964
|
+
catch { /* already exited */ }
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
742
968
|
export function getPiAvailableModels(providerId = 'pi') {
|
|
743
969
|
return [...new Set([...conversations.values()]
|
|
744
970
|
.filter(state => state.providerId === providerId)
|
|
@@ -871,19 +1097,23 @@ export function handlePiChat(conversationId, text, workDir, options, files) {
|
|
|
871
1097
|
emitPiError(state, 'PI is already processing this conversation.');
|
|
872
1098
|
return;
|
|
873
1099
|
}
|
|
874
|
-
|
|
1100
|
+
const resolvedWorkDir = resolve(workDir);
|
|
1101
|
+
const workDirChanged = normalizePathForCompare(state.workDir) !== normalizePathForCompare(resolvedWorkDir);
|
|
875
1102
|
const requestedProviderId = options?.providerId || state.providerId;
|
|
876
1103
|
const providerChanged = state.providerId !== requestedProviderId;
|
|
877
|
-
if (providerChanged) {
|
|
1104
|
+
if (providerChanged || workDirChanged) {
|
|
878
1105
|
if (state.child)
|
|
879
1106
|
terminatePiChild(state);
|
|
1107
|
+
state.workDir = resolvedWorkDir;
|
|
1108
|
+
state.commands = [];
|
|
880
1109
|
state.piSessionId = null;
|
|
881
1110
|
state.lastPiSessionId = null;
|
|
882
1111
|
state.sessionFile = null;
|
|
883
1112
|
state.sessionNotified = false;
|
|
884
1113
|
state.model = null;
|
|
885
1114
|
state.thinking = null;
|
|
886
|
-
|
|
1115
|
+
if (providerChanged)
|
|
1116
|
+
state.availableModels = [];
|
|
887
1117
|
}
|
|
888
1118
|
else if (state.child && state.childProviderId !== requestedProviderId) {
|
|
889
1119
|
terminatePiChild(state);
|