a2acalling 0.6.41 → 0.6.43
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/bin/cli.js +160 -32
- package/package.json +1 -1
- package/src/lib/conversation-driver.js +30 -0
package/bin/cli.js
CHANGED
|
@@ -71,6 +71,13 @@ function getConvStore() {
|
|
|
71
71
|
|
|
72
72
|
const store = new TokenStore();
|
|
73
73
|
|
|
74
|
+
// Commands that should hard-fail with a clear error when not onboarded,
|
|
75
|
+
// rather than falling through to the interactive quickstart flow.
|
|
76
|
+
// These are outbound operations often invoked by agents/automation.
|
|
77
|
+
const ONBOARDING_HARD_FAIL = new Set([
|
|
78
|
+
'call', 'ping', 'status'
|
|
79
|
+
]);
|
|
80
|
+
|
|
74
81
|
// ── enforceOnboarding ────────────────────────────────────────────────────
|
|
75
82
|
// If onboarding is incomplete or the config is missing/invalid, run the
|
|
76
83
|
// full quickstart flow inline — verbose, with direct stdio. The agent sees
|
|
@@ -89,6 +96,14 @@ function enforceOnboarding(command) {
|
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
if (!isOnboarded()) {
|
|
99
|
+
// For outbound commands (call, ping, status), fail immediately with
|
|
100
|
+
// a clear error instead of dumping onboarding prompts. This prevents
|
|
101
|
+
// calling agents from receiving walls of setup instructions.
|
|
102
|
+
if (ONBOARDING_HARD_FAIL.has(command)) {
|
|
103
|
+
console.error('❌ Onboarding not complete. Run `a2a quickstart` first to set up your agent.');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
// Run the full quickstart flow inline — verbose output, direct stdio.
|
|
93
108
|
// This replaces the original command; after onboarding the agent can
|
|
94
109
|
// re-run their intended command.
|
|
@@ -2022,60 +2037,173 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
2022
2037
|
}
|
|
2023
2038
|
}
|
|
2024
2039
|
|
|
2025
|
-
//
|
|
2026
|
-
function
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
|
|
2040
|
+
// Check if a TCP port is currently occupied (async, fast)
|
|
2041
|
+
function isPortOccupied(port) {
|
|
2042
|
+
if (!port) return false;
|
|
2043
|
+
const net = require('net');
|
|
2044
|
+
return new Promise((resolve) => {
|
|
2045
|
+
const socket = net.connect({ host: '127.0.0.1', port });
|
|
2046
|
+
let settled = false;
|
|
2047
|
+
const finish = (result) => {
|
|
2048
|
+
if (settled) return;
|
|
2049
|
+
settled = true;
|
|
2050
|
+
try { socket.destroy(); } catch (e) {}
|
|
2051
|
+
resolve(result);
|
|
2052
|
+
};
|
|
2053
|
+
socket.setTimeout(500, () => finish(false));
|
|
2054
|
+
socket.once('connect', () => finish(true));
|
|
2055
|
+
socket.once('error', () => finish(false));
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2033
2058
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2059
|
+
// Find PID listening on a given port by parsing /proc/net/tcp6 (Linux)
|
|
2060
|
+
function findPidOnPort(port) {
|
|
2061
|
+
try {
|
|
2062
|
+
// Try fuser first (most reliable)
|
|
2063
|
+
const fuserResult = spawnSync('fuser', [`${port}/tcp`], {
|
|
2064
|
+
encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
2065
|
+
});
|
|
2066
|
+
if (fuserResult.status === 0 && fuserResult.stdout) {
|
|
2067
|
+
const pids = fuserResult.stdout.trim().split(/\s+/).map(Number).filter(p => p > 0);
|
|
2068
|
+
if (pids.length > 0) return pids;
|
|
2040
2069
|
}
|
|
2070
|
+
} catch (e) { /* fuser not available, fall through */ }
|
|
2041
2071
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
const
|
|
2047
|
-
|
|
2048
|
-
try {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2072
|
+
try {
|
|
2073
|
+
// Fallback: parse /proc/net/tcp and /proc/net/tcp6
|
|
2074
|
+
const portHex = port.toString(16).toUpperCase().padStart(4, '0');
|
|
2075
|
+
const pids = [];
|
|
2076
|
+
for (const proto of ['/proc/net/tcp', '/proc/net/tcp6']) {
|
|
2077
|
+
let content;
|
|
2078
|
+
try { content = fs.readFileSync(proto, 'utf8'); } catch (e) { continue; }
|
|
2079
|
+
const lines = content.split('\n');
|
|
2080
|
+
for (const line of lines) {
|
|
2081
|
+
const parts = line.trim().split(/\s+/);
|
|
2082
|
+
if (parts.length < 10) continue;
|
|
2083
|
+
const localAddr = parts[1]; // e.g., 00000000:1F90
|
|
2084
|
+
const localPort = localAddr.split(':')[1];
|
|
2085
|
+
if (localPort && localPort.toUpperCase() === portHex) {
|
|
2086
|
+
const inode = parts[9];
|
|
2087
|
+
// Search /proc/*/fd/* for socket inodes
|
|
2088
|
+
try {
|
|
2089
|
+
const procDirs = fs.readdirSync('/proc').filter(d => /^\d+$/.test(d));
|
|
2090
|
+
for (const pid of procDirs) {
|
|
2091
|
+
try {
|
|
2092
|
+
const fdDir = `/proc/${pid}/fd`;
|
|
2093
|
+
const fds = fs.readdirSync(fdDir);
|
|
2094
|
+
for (const fd of fds) {
|
|
2095
|
+
try {
|
|
2096
|
+
const link = fs.readlinkSync(`${fdDir}/${fd}`);
|
|
2097
|
+
if (link === `socket:[${inode}]`) {
|
|
2098
|
+
pids.push(Number(pid));
|
|
2099
|
+
}
|
|
2100
|
+
} catch (e) {}
|
|
2101
|
+
}
|
|
2102
|
+
} catch (e) {}
|
|
2103
|
+
}
|
|
2104
|
+
} catch (e) {}
|
|
2105
|
+
}
|
|
2054
2106
|
}
|
|
2055
2107
|
}
|
|
2108
|
+
if (pids.length > 0) return [...new Set(pids)];
|
|
2109
|
+
} catch (e) { /* /proc parsing failed */ }
|
|
2110
|
+
|
|
2111
|
+
return [];
|
|
2112
|
+
}
|
|
2056
2113
|
|
|
2057
|
-
|
|
2114
|
+
// Kill a specific PID with SIGTERM, wait, then SIGKILL if needed
|
|
2115
|
+
function killPidSync(pid) {
|
|
2116
|
+
try {
|
|
2117
|
+
process.kill(pid, 0); // existence check
|
|
2118
|
+
} catch (e) {
|
|
2119
|
+
return { ok: true, skipped: true };
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
process.kill(pid, 'SIGTERM');
|
|
2123
|
+
const start = Date.now();
|
|
2124
|
+
while (Date.now() - start < 3000) {
|
|
2058
2125
|
try {
|
|
2059
|
-
process.kill(pid,
|
|
2060
|
-
|
|
2126
|
+
process.kill(pid, 0);
|
|
2127
|
+
spawnSync('sleep', ['0.1'], { timeout: 500 });
|
|
2061
2128
|
} catch (e) {
|
|
2062
2129
|
return { ok: true, pid };
|
|
2063
2130
|
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// Still alive — force kill
|
|
2134
|
+
try {
|
|
2135
|
+
process.kill(pid, 'SIGKILL');
|
|
2136
|
+
// Brief wait for SIGKILL to take effect
|
|
2137
|
+
spawnSync('sleep', ['0.2'], { timeout: 500 });
|
|
2138
|
+
try {
|
|
2139
|
+
process.kill(pid, 0);
|
|
2140
|
+
return { ok: false, pid, error: `PID ${pid} survived SIGKILL` };
|
|
2141
|
+
} catch (e) {
|
|
2142
|
+
return { ok: true, pid, forced: true };
|
|
2143
|
+
}
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
return { ok: true, pid };
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// Kill server by PID from config (detached process started by quickstart)
|
|
2150
|
+
// Then verify the port is actually freed; if not, find and kill whatever holds it.
|
|
2151
|
+
async function killServerPid() {
|
|
2152
|
+
let pid, serverPort;
|
|
2153
|
+
try {
|
|
2154
|
+
const { A2AConfig } = require('../src/lib/config');
|
|
2155
|
+
const cfg = new A2AConfig();
|
|
2156
|
+
const onboarding = cfg.getOnboarding();
|
|
2157
|
+
pid = onboarding.server_pid;
|
|
2158
|
+
serverPort = onboarding.server_port;
|
|
2064
2159
|
} catch (err) {
|
|
2065
2160
|
// Config read failed — not fatal, continue with pm2 path
|
|
2066
2161
|
return { ok: true, skipped: true };
|
|
2067
2162
|
}
|
|
2163
|
+
|
|
2164
|
+
// Step 1: Try to kill the PID from config
|
|
2165
|
+
if (pid) {
|
|
2166
|
+
killPidSync(pid);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// Step 2: Verify the port is freed
|
|
2170
|
+
if (serverPort) {
|
|
2171
|
+
const stillOccupied = await isPortOccupied(serverPort);
|
|
2172
|
+
if (stillOccupied) {
|
|
2173
|
+
// Port is still held — find and kill whatever is on it
|
|
2174
|
+
const pids = findPidOnPort(serverPort);
|
|
2175
|
+
let killedAny = false;
|
|
2176
|
+
for (const p of pids) {
|
|
2177
|
+
const result = killPidSync(p);
|
|
2178
|
+
if (result.ok && !result.skipped) killedAny = true;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// Final check
|
|
2182
|
+
const stillUp = await isPortOccupied(serverPort);
|
|
2183
|
+
if (stillUp) {
|
|
2184
|
+
return { ok: false, pid, port: serverPort, error: `Port ${serverPort} is still occupied after kill attempts` };
|
|
2185
|
+
}
|
|
2186
|
+
return { ok: true, pid, port: serverPort, portKill: killedAny };
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
return { ok: true, pid, port: serverPort, skipped: !pid };
|
|
2068
2191
|
}
|
|
2069
2192
|
|
|
2070
2193
|
process.stdout.write('Stopping server... ');
|
|
2071
|
-
const pidResult = killServerPid();
|
|
2194
|
+
const pidResult = await killServerPid();
|
|
2072
2195
|
const stopped = pm2StopAndDelete('a2a');
|
|
2073
2196
|
if (!pidResult.ok && !stopped.ok) {
|
|
2074
2197
|
console.log('❌');
|
|
2075
|
-
console.error(` ${stopped.error}`);
|
|
2198
|
+
console.error(` ${pidResult.error || stopped.error}`);
|
|
2076
2199
|
process.exit(1);
|
|
2077
2200
|
}
|
|
2078
|
-
|
|
2201
|
+
if (!pidResult.ok) {
|
|
2202
|
+
console.log('⚠️');
|
|
2203
|
+
console.error(` Warning: ${pidResult.error}`);
|
|
2204
|
+
} else {
|
|
2205
|
+
console.log('✅');
|
|
2206
|
+
}
|
|
2079
2207
|
|
|
2080
2208
|
let configOk = true;
|
|
2081
2209
|
let dbOk = true;
|
package/package.json
CHANGED
|
@@ -33,6 +33,11 @@ const TERMINATION_PATTERNS = [
|
|
|
33
33
|
/\[DISCONNECT/i,
|
|
34
34
|
/\[END.?CALL\]/i,
|
|
35
35
|
/\[CLOSING\]/i,
|
|
36
|
+
/\bEND.?CALL\b/i,
|
|
37
|
+
/\bcall\s+closed\b/i,
|
|
38
|
+
/\bwrapping\s+up\b/i,
|
|
39
|
+
/\[No\s+further\b/i,
|
|
40
|
+
/\bno\s+further\b/i,
|
|
36
41
|
/\bREFUSING\s+(TO\s+)?(CONTINU|RESPOND|ENGAG)/i,
|
|
37
42
|
/\bcall\s+complet(ed|e)\b/i,
|
|
38
43
|
/\bconversation\s+(is\s+)?(over|ended|closed|complet)/i,
|
|
@@ -43,6 +48,9 @@ const TERMINATION_PATTERNS = [
|
|
|
43
48
|
|
|
44
49
|
function detectRemoteTermination(text) {
|
|
45
50
|
if (!text || typeof text !== 'string') return false;
|
|
51
|
+
// Very short responses (single dot, empty-ish) indicate dead conversation
|
|
52
|
+
const trimmed = text.trim();
|
|
53
|
+
if (trimmed.length <= 1) return true;
|
|
46
54
|
return TERMINATION_PATTERNS.some(pattern => pattern.test(text));
|
|
47
55
|
}
|
|
48
56
|
|
|
@@ -82,6 +90,12 @@ function inferStateProgression(collabState, remoteText, turn) {
|
|
|
82
90
|
// Confidence increases over turns
|
|
83
91
|
patch.confidence = Math.min(0.9, 0.25 + turn * 0.08);
|
|
84
92
|
|
|
93
|
+
// In converging phase, signal close — conversation has run its natural course
|
|
94
|
+
const effectivePhase = patch.phase || collabState.phase;
|
|
95
|
+
if (effectivePhase === 'converging') {
|
|
96
|
+
patch.closeSignal = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
return patch;
|
|
86
100
|
}
|
|
87
101
|
|
|
@@ -207,6 +221,7 @@ Be concise but specific. No filler.`;
|
|
|
207
221
|
conversationId = `conv_${Date.now()}_local`;
|
|
208
222
|
|
|
209
223
|
let nextMessage = openingMessage;
|
|
224
|
+
const overlapHistory = [];
|
|
210
225
|
|
|
211
226
|
for (let turn = 0; turn < this.maxTurns; turn++) {
|
|
212
227
|
// 1. Send message to remote
|
|
@@ -385,6 +400,21 @@ Be concise but specific. No filler.`;
|
|
|
385
400
|
if (inferred.phase) collabState.phase = inferred.phase;
|
|
386
401
|
if (inferred.overlapScore != null) collabState.overlapScore = inferred.overlapScore;
|
|
387
402
|
if (inferred.confidence != null) collabState.confidence = inferred.confidence;
|
|
403
|
+
if (inferred.closeSignal != null) collabState.closeSignal = inferred.closeSignal;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 6b. Overlap flatline detection — if overlap hasn't changed significantly
|
|
407
|
+
// for 3+ consecutive turns while in converging phase, the conversation is dead
|
|
408
|
+
overlapHistory.push(collabState.overlapScore);
|
|
409
|
+
if (collabState.phase === 'converging' && overlapHistory.length >= 3) {
|
|
410
|
+
const recent = overlapHistory.slice(-3);
|
|
411
|
+
const maxDelta = Math.max(
|
|
412
|
+
Math.abs(recent[1] - recent[0]),
|
|
413
|
+
Math.abs(recent[2] - recent[1])
|
|
414
|
+
);
|
|
415
|
+
if (maxDelta < 0.02) {
|
|
416
|
+
collabState.closeSignal = true;
|
|
417
|
+
}
|
|
388
418
|
}
|
|
389
419
|
|
|
390
420
|
// 7. Persist collab state to DB
|