@fastino-ai/pioneer-cli 0.2.8 → 0.2.9

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 CHANGED
@@ -88,6 +88,8 @@ pioneer model_artifacts list
88
88
 
89
89
  `agent` starts in interactive mode by default (standard workflow). For the only explicit alternate mode:
90
90
  - `--mode research`: Pro mode with deeper research response style.
91
+ - `agent resume`: list recent conversations and resume a selected session.
92
+ - `agent sessions`: explicit alias for listing sessions and resuming one.
91
93
 
92
94
  ```bash
93
95
  # Interactive (default)
@@ -98,6 +100,15 @@ pioneer agent
98
100
 
99
101
  # Research mode (Pro subscription required)
100
102
  pioneer agent --mode research
103
+
104
+ # Open a session list and pick one to continue
105
+ pioneer agent resume
106
+
107
+ # Equivalent: explicit sessions command
108
+ pioneer agent sessions
109
+
110
+ # Resume a specific conversation id
111
+ pioneer agent resume 4f2a...
101
112
  ```
102
113
 
103
114
  When you start any of these commands, the CLI opens a conversational prompt and keeps accepting follow-up messages in the same session.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastino-ai/pioneer-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Pioneer CLI - AI training platform with chat agent",
5
5
  "type": "module",
6
6
  "files": [
@@ -36,6 +36,7 @@
36
36
  "@types/react": "^18.3.0",
37
37
  "@types/ws": "^8.18.1",
38
38
  "bun-types": "^1.1.0",
39
+ "ink-testing-library": "^4.0.0",
39
40
  "typescript": "^5.6.3"
40
41
  },
41
42
  "engines": {
@@ -855,7 +855,8 @@ File references:
855
855
  }, [client, state.isProcessing]);
856
856
 
857
857
  // Handle keyboard shortcuts
858
- useInput((char, key) => {
858
+ useInput(
859
+ (char, key) => {
859
860
  if (key.ctrl && char === "c") {
860
861
  exit();
861
862
  return;
@@ -920,7 +921,9 @@ File references:
920
921
  return;
921
922
  }
922
923
  }
923
- });
924
+ },
925
+ { isActive: isRawModeSupported }
926
+ );
924
927
 
925
928
  if (!client) {
926
929
  return (
package/src/config.ts CHANGED
@@ -23,8 +23,12 @@ export interface Config {
23
23
  // Hugging Face token for pushing datasets/models
24
24
  hfToken?: string;
25
25
 
26
+ // Last agent conversation ID used for resuming chats
27
+ lastAgentConversationId?: string;
28
+
26
29
  // Telemetry (opt-in analytics)
27
30
  telemetry?: TelemetryConfig;
31
+
28
32
  }
29
33
 
30
34
  const CONFIG_DIR = path.join(os.homedir(), ".pioneer");
@@ -169,6 +173,17 @@ export function getBaseUrl(): string {
169
173
  return DEFAULT_BASE_URL;
170
174
  }
171
175
 
176
+ export function getLastAgentConversationId(): string | undefined {
177
+ return loadConfig().lastAgentConversationId;
178
+ }
179
+
180
+ export function setLastAgentConversationId(conversationId?: string): void {
181
+ if (!conversationId) {
182
+ return;
183
+ }
184
+ saveConfig({ lastAgentConversationId: conversationId });
185
+ }
186
+
172
187
  export function getMleModel(): string | undefined {
173
188
  return loadConfig().mleModel;
174
189
  }
package/src/index.tsx CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  getApiKey,
16
16
  getBaseUrl,
17
17
  getMleModel,
18
+ getLastAgentConversationId,
19
+ setLastAgentConversationId,
18
20
  saveConfig,
19
21
  clearApiKey,
20
22
  getHfToken,
@@ -274,24 +276,28 @@ interface TelemetryPromptProps {
274
276
 
275
277
  const TelemetryPrompt: React.FC<TelemetryPromptProps> = ({ onComplete }) => {
276
278
  const [selected, setSelected] = useState<"yes" | "no">("yes");
279
+ const { isRawModeSupported } = useStdin();
277
280
 
278
- useInput((input, key) => {
279
- if (key.leftArrow || key.rightArrow) {
280
- setSelected((s) => (s === "yes" ? "no" : "yes"));
281
- }
282
- if (key.return) {
283
- setTelemetryEnabled(selected === "yes");
284
- onComplete();
285
- }
286
- if (input === "y" || input === "Y") {
287
- setTelemetryEnabled(true);
288
- onComplete();
289
- }
290
- if (input === "n" || input === "N") {
291
- setTelemetryEnabled(false);
292
- onComplete();
293
- }
294
- });
281
+ useInput(
282
+ (input, key) => {
283
+ if (key.leftArrow || key.rightArrow) {
284
+ setSelected((s) => (s === "yes" ? "no" : "yes"));
285
+ }
286
+ if (key.return) {
287
+ setTelemetryEnabled(selected === "yes");
288
+ onComplete();
289
+ }
290
+ if (input === "y" || input === "Y") {
291
+ setTelemetryEnabled(true);
292
+ onComplete();
293
+ }
294
+ if (input === "n" || input === "N") {
295
+ setTelemetryEnabled(false);
296
+ onComplete();
297
+ }
298
+ },
299
+ { isActive: isRawModeSupported }
300
+ );
295
301
 
296
302
  return (
297
303
  <Box flexDirection="column" paddingX={1}>
@@ -725,8 +731,12 @@ function toHistoryMessages(
725
731
  return items
726
732
  .filter((item) => Boolean(item && typeof item.content === "string" && typeof item.role === "string"))
727
733
  .map((item) => {
734
+ const role =
735
+ item.role === "user" || item.role === "assistant" || item.role === "tool"
736
+ ? item.role
737
+ : "assistant";
728
738
  const message: HistoryMessage = {
729
- role: item.role as "user" | "assistant" | "tool" | string,
739
+ role,
730
740
  content: item.content,
731
741
  };
732
742
 
@@ -735,7 +745,20 @@ function toHistoryMessages(
735
745
  message.tool_call_id = sessionMessage.tool_call_id;
736
746
  }
737
747
  if (sessionMessage.tool_calls && Array.isArray(sessionMessage.tool_calls)) {
738
- message.tool_calls = sessionMessage.tool_calls.filter((call) => Boolean(call));
748
+ message.tool_calls = sessionMessage.tool_calls
749
+ .filter(
750
+ (call): call is { id: string; name: string; args: Record<string, unknown> } =>
751
+ Boolean(call) &&
752
+ typeof call.id === "string" &&
753
+ typeof call.name === "string" &&
754
+ typeof call.args === "object" &&
755
+ call.args !== null
756
+ )
757
+ .map((call) => ({
758
+ id: call.id,
759
+ name: call.name,
760
+ args: call.args,
761
+ }));
739
762
  }
740
763
 
741
764
  return message;
@@ -757,6 +780,11 @@ function AgentInteractiveCommand({ message, conversationId, history }: AgentInte
757
780
  const finish = (code: number) => {
758
781
  setTimeout(() => process.exit(code), 300);
759
782
  };
783
+ const persistConversation = (nextConversationId?: string) => {
784
+ if (nextConversationId) {
785
+ setLastAgentConversationId(nextConversationId);
786
+ }
787
+ };
760
788
  const getLatestAssistantContent = (
761
789
  messages: HistoryMessage[] | undefined
762
790
  ): string => {
@@ -838,6 +866,9 @@ function AgentInteractiveCommand({ message, conversationId, history }: AgentInte
838
866
  throw new Error(lastError || result.error || "Agent fallback request failed.");
839
867
  }
840
868
  setStream(result.data?.answer ?? "");
869
+ if (result.data?.conversation_id) {
870
+ persistConversation(result.data.conversation_id);
871
+ }
841
872
  if (!result.data?.answer) {
842
873
  setError("Agent returned no response content.");
843
874
  setState("error");
@@ -982,6 +1013,7 @@ function AgentInteractiveCommand({ message, conversationId, history }: AgentInte
982
1013
  }
983
1014
  doneHistory.current = messages;
984
1015
  wsSessionId = sessionId;
1016
+ persistConversation(sessionId);
985
1017
  },
986
1018
  }, {
987
1019
  history: mergedHistory,
@@ -1089,59 +1121,244 @@ function formatAutoAgentTurn(turn: AutoAgentTurn): string {
1089
1121
  return `${prefix}${turn.content}`;
1090
1122
  }
1091
1123
 
1092
- function AutoAgentInteractiveSession({
1124
+ export function AutoAgentInteractiveSession({
1093
1125
  conversationId: initialConversationId,
1094
1126
  history,
1095
1127
  mode,
1096
1128
  firstMessage,
1129
+ allowSessionCreation = true,
1097
1130
  }: {
1098
1131
  conversationId?: string;
1099
1132
  history?: api.AgentChatHistoryItem[];
1100
1133
  mode: "standard" | "research";
1101
1134
  firstMessage?: string;
1135
+ allowSessionCreation?: boolean;
1102
1136
  }) {
1137
+ const { isRawModeSupported } = useStdin();
1103
1138
  const [input, setInput] = useState("");
1104
1139
  const [isLoading, setIsLoading] = useState(false);
1105
1140
  const [error, setError] = useState("");
1106
1141
  const [statusHint, setStatusHint] = useState("Start typing...");
1107
1142
  const [conversationId, setConversationId] = useState(initialConversationId);
1108
1143
  const [historyState, setHistoryState] = useState<api.AgentChatHistoryItem[]>(history ?? []);
1144
+ const normalizeTurnRole = (role: string): AutoAgentTurnRole => {
1145
+ const normalized = role?.trim().toLowerCase();
1146
+ return normalized === "user" || normalized === "human" ? "user" : "assistant";
1147
+ };
1148
+ const [isHistoryLoading, setIsHistoryLoading] = useState(
1149
+ Boolean(initialConversationId) && history === undefined
1150
+ );
1151
+ const [isResumeSessionUsable, setIsResumeSessionUsable] = useState(true);
1109
1152
  const [turns, setTurns] = useState<AutoAgentTurn[]>(
1110
- (history ?? []).map((entry) =>
1111
- entry.role === "assistant"
1112
- ? { role: "assistant", content: entry.content }
1113
- : { role: "user", content: entry.content }
1114
- )
1153
+ (history ?? []).map((entry) => ({
1154
+ role: normalizeTurnRole(entry.role),
1155
+ content: entry.content,
1156
+ }))
1115
1157
  );
1158
+ const [inputHistory, setInputHistory] = useState<string[]>([]);
1159
+ const [inputHistoryIndex, setInputHistoryIndex] = useState(-1);
1116
1160
  const didSeedFirstMessage = useRef(false);
1161
+ const setInputValue = (nextValue: string) => {
1162
+ setInput(nextValue);
1163
+ setInputHistoryIndex(-1);
1164
+ };
1165
+ const normalizeSessionUserHistory = (items: api.AgentChatHistoryItem[]) =>
1166
+ items
1167
+ .filter((message) => {
1168
+ const normalizedRole = message.role?.trim().toLowerCase();
1169
+ return (
1170
+ (normalizedRole === "user" || normalizedRole === "human") &&
1171
+ typeof message.content === "string"
1172
+ );
1173
+ })
1174
+ .map((message) => message.content.trim())
1175
+ .filter((content): content is string => content.length > 0);
1176
+ const mapHistoryToTurns = (items: api.AgentChatHistoryItem[]) =>
1177
+ items.map((entry) => ({
1178
+ role: normalizeTurnRole(entry.role),
1179
+ content: entry.content,
1180
+ }));
1181
+
1182
+ const commitInputToHistory = (message: string) => {
1183
+ const normalized = message.trim();
1184
+ if (!normalized) {
1185
+ return;
1186
+ }
1187
+
1188
+ setInputHistory((previous) => {
1189
+ if (previous.length > 0 && previous[previous.length - 1] === normalized) {
1190
+ return previous;
1191
+ }
1192
+ return [...previous, normalized];
1193
+ });
1194
+ };
1195
+
1196
+ useEffect(() => {
1197
+ const restoredHistory = history ?? [];
1198
+ setHistoryState(restoredHistory);
1199
+ setTurns(mapHistoryToTurns(restoredHistory));
1200
+ setInputHistory(normalizeSessionUserHistory(restoredHistory));
1201
+ if (history !== undefined || !initialConversationId) {
1202
+ setIsHistoryLoading(false);
1203
+ }
1204
+ }, [history]);
1205
+
1206
+ useEffect(() => {
1207
+ setTurns(mapHistoryToTurns(historyState));
1208
+ }, [historyState]);
1209
+
1210
+ useEffect(() => {
1211
+ if (!initialConversationId || history !== undefined) {
1212
+ setIsHistoryLoading(false);
1213
+ return;
1214
+ }
1215
+
1216
+ let isActive = true;
1217
+ const hydrateSessionHistory = async () => {
1218
+ const result = await api.getAgentSession(initialConversationId);
1219
+ if (!isActive) {
1220
+ return;
1221
+ }
1222
+
1223
+ if (!result.ok) {
1224
+ const lowerError = (result.error || "").toLowerCase();
1225
+ const notFound =
1226
+ result.status === 404 ||
1227
+ lowerError.includes("session not found") ||
1228
+ lowerError.includes("not found") ||
1229
+ lowerError.includes("does not exist");
1230
+ if (notFound) {
1231
+ setHistoryState([]);
1232
+ setTurns([]);
1233
+ setError("Could not load session history. This session could not be found on the backend. Resume is not available.");
1234
+ setIsResumeSessionUsable(false);
1235
+ setIsHistoryLoading(false);
1236
+ return;
1237
+ }
1238
+ setError(
1239
+ `Could not load session history for ${initialConversationId}: ${result.error || "Unable to load conversation history."}` +
1240
+ " Proceeding without preloaded history."
1241
+ );
1242
+ setIsResumeSessionUsable(true);
1243
+ setIsHistoryLoading(false);
1244
+ return;
1245
+ }
1246
+
1247
+ const restoredMessages = (result.data?.messages ?? [])
1248
+ .map((message) => {
1249
+ if (
1250
+ typeof message.role !== "string" ||
1251
+ typeof message.content !== "string"
1252
+ ) {
1253
+ return undefined;
1254
+ }
1255
+
1256
+ return {
1257
+ role: message.role,
1258
+ content: message.content,
1259
+ } as api.AgentChatHistoryItem;
1260
+ })
1261
+ .filter((message): message is api.AgentChatHistoryItem => message !== undefined);
1262
+
1263
+ setHistoryState(restoredMessages);
1264
+ setTurns(mapHistoryToTurns(restoredMessages));
1265
+ setInputHistory(normalizeSessionUserHistory(restoredMessages));
1266
+
1267
+ setIsHistoryLoading(false);
1268
+ };
1269
+
1270
+ void hydrateSessionHistory();
1271
+
1272
+ return () => {
1273
+ isActive = false;
1274
+ };
1275
+ }, [initialConversationId, history]);
1117
1276
 
1118
1277
  const runAutoTurn = async (rawMessage: string) => {
1278
+ if (isHistoryLoading) {
1279
+ return;
1280
+ }
1119
1281
  const trimmed = rawMessage.trim();
1120
1282
  if (!trimmed || isLoading) {
1121
1283
  return;
1122
1284
  }
1285
+ commitInputToHistory(trimmed);
1286
+ if (!isResumeSessionUsable && !allowSessionCreation) {
1287
+ setError("Unable to resume this session because the conversation record is unavailable.");
1288
+ setIsLoading(false);
1289
+ setStatusHint("Ready for next message.");
1290
+ return;
1291
+ }
1123
1292
 
1124
1293
  const nextHistory: api.AgentChatHistoryItem[] = [
1125
1294
  ...historyState,
1126
1295
  { role: "user", content: trimmed },
1127
1296
  ];
1128
1297
  setHistoryState(nextHistory);
1129
- setTurns((current) => [...current, { role: "user", content: trimmed }]);
1130
- setInput("");
1298
+ setInputValue("");
1131
1299
  setError("");
1132
1300
  setIsLoading(true);
1133
1301
  setStatusHint("Thinking...");
1134
1302
 
1135
- const result = await api.agentChat({
1136
- message: trimmed,
1137
- ...(conversationId ? { conversation_id: conversationId } : {}),
1138
- ...(nextHistory.length ? { history: nextHistory } : {}),
1139
- });
1303
+ let activeConversationId = conversationId;
1304
+
1305
+ if (!activeConversationId && !allowSessionCreation) {
1306
+ setError("Unable to resume this session because no valid conversation id is available.");
1307
+ setIsLoading(false);
1308
+ setStatusHint("Ready for next message.");
1309
+ return;
1310
+ }
1311
+
1312
+ if (!activeConversationId) {
1313
+ setStatusHint("Creating conversation...");
1314
+ const createdSession = await api.createAgentSession({
1315
+ first_message: trimmed,
1316
+ title: trimmed.slice(0, 80) || "New agent session",
1317
+ });
1318
+ if (!createdSession.ok) {
1319
+ setError(`Failed to create conversation: ${createdSession.error || "Unknown error."}`);
1320
+ setIsLoading(false);
1321
+ setStatusHint("Ready for next message.");
1322
+ return;
1323
+ }
1324
+ const createdSessionId = createdSession.data?.id;
1325
+ if (!createdSessionId) {
1326
+ setError("Failed to create conversation: Missing conversation id from server response.");
1327
+ setIsLoading(false);
1328
+ setStatusHint("Ready for next message.");
1329
+ return;
1330
+ }
1331
+ activeConversationId = createdSessionId;
1332
+ setConversationId(activeConversationId);
1333
+ persistConversation(activeConversationId);
1334
+ }
1335
+
1336
+ const sendTurn = async (requestConversationId: string | undefined) =>
1337
+ api.agentChat({
1338
+ message: trimmed,
1339
+ ...(requestConversationId ? { conversation_id: requestConversationId } : {}),
1340
+ ...(nextHistory.length ? { history: nextHistory } : {}),
1341
+ });
1342
+
1343
+ let result = await sendTurn(activeConversationId);
1344
+ const lowerError = (result.error || "").toLowerCase();
1345
+ const notFound =
1346
+ result.status === 404 ||
1347
+ lowerError.includes("session not found") ||
1348
+ lowerError.includes("not found") ||
1349
+ lowerError.includes("does not exist");
1140
1350
 
1141
1351
  if (!result.ok) {
1142
- if (
1352
+ if (lowerError.includes("failed to get session") && notFound) {
1353
+ if (!allowSessionCreation) {
1354
+ setError("Unable to resume this session. It is not available on the backend.");
1355
+ setIsResumeSessionUsable(false);
1356
+ } else {
1357
+ setError("Failed to resume session on the backend. Please start a new message to create a new session.");
1358
+ }
1359
+ } else if (
1143
1360
  result.status === 403 &&
1144
- (result.error ?? "").toLowerCase().includes("deep research mode")
1361
+ lowerError.includes("deep research mode")
1145
1362
  ) {
1146
1363
  setError(
1147
1364
  "Research mode requires a Pro subscription.\n" +
@@ -1151,29 +1368,121 @@ function AutoAgentInteractiveSession({
1151
1368
  } else {
1152
1369
  setError(result.error || "Agent request failed.");
1153
1370
  }
1371
+
1154
1372
  setIsLoading(false);
1155
1373
  setStatusHint("Ready for next message.");
1156
1374
  return;
1157
1375
  }
1158
1376
 
1159
- const answer = result.data.answer?.trim() || "No response content yet.";
1160
- const nextConversationId = result.data.conversation_id || conversationId;
1377
+ const data = result.data;
1378
+ if (!data) {
1379
+ setError("Agent request did not return a usable response.");
1380
+ setIsLoading(false);
1381
+ setStatusHint("Ready for next message.");
1382
+ return;
1383
+ }
1161
1384
 
1162
- setConversationId(nextConversationId);
1163
- setTurns((current) => [...current, { role: "assistant", content: answer }]);
1164
- setHistoryState((current) => [...current, { role: "assistant", content: answer }]);
1385
+ const answer = data.answer?.trim() || "No response content yet.";
1386
+ setError("");
1387
+ const persistedConversationId = activeConversationId ?? data.conversation_id;
1388
+ if (persistedConversationId) {
1389
+ setConversationId(persistedConversationId);
1390
+ persistConversation(persistedConversationId);
1391
+ }
1392
+
1393
+ const assistantTurn: api.AgentChatHistoryItem = { role: "assistant", content: answer };
1394
+ if (persistedConversationId) {
1395
+ void api.appendSessionMessages(persistedConversationId, {
1396
+ messages: [
1397
+ { role: "user", content: trimmed },
1398
+ { role: "assistant", content: answer },
1399
+ ],
1400
+ });
1401
+ }
1402
+ setHistoryState((current) => [...current, assistantTurn]);
1165
1403
  setStatusHint("Ready for next message.");
1166
1404
  setIsLoading(false);
1167
1405
  };
1168
1406
 
1407
+ useInput(
1408
+ (character, key) => {
1409
+ if (key.return) {
1410
+ void runAutoTurn(input);
1411
+ return;
1412
+ }
1413
+
1414
+ if (key.backspace || key.delete) {
1415
+ setInputValue(input.slice(0, -1));
1416
+ return;
1417
+ }
1418
+
1419
+ if (key.upArrow) {
1420
+ if (!inputHistory.length) {
1421
+ return;
1422
+ }
1423
+ const nextIndex =
1424
+ inputHistoryIndex === -1 ? inputHistory.length - 1 : Math.max(0, inputHistoryIndex - 1);
1425
+ setInputHistoryIndex(nextIndex);
1426
+ setInput(inputHistory[nextIndex] ?? "");
1427
+ return;
1428
+ }
1429
+
1430
+ if (key.downArrow) {
1431
+ if (inputHistoryIndex === -1) {
1432
+ return;
1433
+ }
1434
+ const nextIndex = inputHistoryIndex + 1;
1435
+ if (nextIndex >= inputHistory.length) {
1436
+ setInputHistoryIndex(-1);
1437
+ setInput("");
1438
+ return;
1439
+ }
1440
+ setInputHistoryIndex(nextIndex);
1441
+ setInput(inputHistory[nextIndex] ?? "");
1442
+ return;
1443
+ }
1444
+
1445
+ if (
1446
+ key.tab ||
1447
+ key.leftArrow ||
1448
+ key.rightArrow ||
1449
+ key.escape ||
1450
+ key.ctrl ||
1451
+ key.meta
1452
+ ) {
1453
+ return;
1454
+ }
1455
+
1456
+ if (character) {
1457
+ setInputValue(input + character);
1458
+ }
1459
+ },
1460
+ { isActive: isRawModeSupported && !isLoading }
1461
+ );
1462
+
1463
+ const persistConversation = (nextConversationId?: string) => {
1464
+ if (nextConversationId) {
1465
+ setLastAgentConversationId(nextConversationId);
1466
+ }
1467
+ };
1468
+
1469
+ useEffect(() => {
1470
+ return () => {
1471
+ persistConversation(conversationId);
1472
+ };
1473
+ }, [conversationId]);
1474
+
1169
1475
  useEffect(() => {
1170
1476
  if (!firstMessage || didSeedFirstMessage.current) {
1171
1477
  return;
1172
1478
  }
1479
+ if (isHistoryLoading) {
1480
+ return;
1481
+ }
1173
1482
 
1174
1483
  didSeedFirstMessage.current = true;
1175
1484
  void runAutoTurn(firstMessage);
1176
- }, [firstMessage]);
1485
+ }, [firstMessage, isHistoryLoading]);
1177
1486
 
1178
1487
  return (
1179
1488
  <Box flexDirection="column">
@@ -1182,6 +1491,7 @@ function AutoAgentInteractiveSession({
1182
1491
  Mode: {mode === "research" ? "research" : "standard"}{" "}
1183
1492
  {conversationId ? `(conversation ${conversationId})` : "(new conversation)"}
1184
1493
  </Text>
1494
+ {isHistoryLoading ? <Loading message="Loading conversation history..." /> : null}
1185
1495
  <Box flexDirection="column" marginTop={1}>
1186
1496
  {turns.map((entry, idx) => (
1187
1497
  <Text key={`agent-turn-${idx}`} dimColor={entry.role === "user"}>
@@ -1194,8 +1504,9 @@ function AutoAgentInteractiveSession({
1194
1504
  {isLoading ? null : (
1195
1505
  <TextInput
1196
1506
  value={input}
1197
- onChange={setInput}
1198
- onSubmit={runAutoTurn}
1507
+ focus={false}
1508
+ onChange={() => {}}
1509
+ onSubmit={() => {}}
1199
1510
  />
1200
1511
  )}
1201
1512
  </Box>
@@ -1209,21 +1520,372 @@ function AutoAgentInteractiveSession({
1209
1520
  );
1210
1521
  }
1211
1522
 
1523
+ export function AgentResumeCommand({
1524
+ mode,
1525
+ }: {
1526
+ mode?: "standard" | "research";
1527
+ }) {
1528
+ const { isRawModeSupported } = useStdin();
1529
+ const [isReady, setReady] = useState(false);
1530
+ const [selectedSession, setSelectedSession] = useState<
1531
+ {
1532
+ id: string;
1533
+ } | undefined
1534
+ >();
1535
+ const [defaultSessionId] = useState(() => getLastAgentConversationId());
1536
+ const [searchQuery, setSearchQuery] = useState("");
1537
+ const [highlightIndex, setHighlightIndex] = useState(0);
1538
+ const [sessions, setSessions] = useState<
1539
+ Array<{
1540
+ id: string;
1541
+ title: string;
1542
+ updated_at: string;
1543
+ is_archived: boolean;
1544
+ }>
1545
+ >([]);
1546
+ const [error, setError] = useState("");
1547
+ const [statusHint, setStatusHint] = useState("Loading sessions...");
1548
+ const [isSessionLoading, setSessionLoading] = useState(false);
1549
+ const [sessionError, setSessionError] = useState("");
1550
+ const [selectedSessionHistory, setSelectedSessionHistory] = useState<
1551
+ api.AgentChatHistoryItem[] | undefined
1552
+ >(undefined);
1553
+
1554
+ useEffect(() => {
1555
+ if (!selectedSession?.id) {
1556
+ setSelectedSessionHistory(undefined);
1557
+ setSessionError("");
1558
+ return;
1559
+ }
1560
+
1561
+ let isActive = true;
1562
+ const loadSessionHistory = async () => {
1563
+ setSessionLoading(true);
1564
+ setSessionError("");
1565
+
1566
+ const result = await api.getAgentSession(selectedSession.id);
1567
+ if (!isActive) {
1568
+ return;
1569
+ }
1570
+
1571
+ if (!result.ok) {
1572
+ setSessionError(
1573
+ `Could not load selected session ${selectedSession.id}: ${result.error || "Unknown error."}`
1574
+ );
1575
+ setSelectedSessionHistory(undefined);
1576
+ setSessionLoading(false);
1577
+ return;
1578
+ }
1579
+
1580
+ const restoredMessages = (result.data?.messages ?? [])
1581
+ .map((message) => {
1582
+ if (typeof message?.role !== "string" || typeof message?.content !== "string") {
1583
+ return undefined;
1584
+ }
1585
+ return {
1586
+ role: message.role,
1587
+ content: message.content,
1588
+ } as api.AgentChatHistoryItem;
1589
+ })
1590
+ .filter((message): message is api.AgentChatHistoryItem => message !== undefined);
1591
+
1592
+ setSelectedSessionHistory(restoredMessages);
1593
+ setSessionLoading(false);
1594
+ };
1595
+
1596
+ void loadSessionHistory();
1597
+
1598
+ return () => {
1599
+ isActive = false;
1600
+ setSessionLoading(false);
1601
+ };
1602
+ }, [selectedSession?.id]);
1603
+
1604
+ useEffect(() => {
1605
+ let isActive = true;
1606
+ const loadSessions = async () => {
1607
+ try {
1608
+ const result = await api.listAgentSessions();
1609
+ if (!isActive) {
1610
+ return;
1611
+ }
1612
+ if (!result.ok) {
1613
+ setError(result.error || "Could not load agent sessions.");
1614
+ setStatusHint("");
1615
+ setReady(true);
1616
+ setSessions([]);
1617
+ return;
1618
+ }
1619
+ const remoteSessions = (result.data?.sessions ?? []).map((session) => ({
1620
+ id: session.id,
1621
+ title: session.title || "(untitled)",
1622
+ updated_at: session.updated_at || new Date().toISOString(),
1623
+ is_archived: session.is_archived,
1624
+ }));
1625
+ remoteSessions.sort((a, b) => {
1626
+ const aTime = Date.parse(a.updated_at);
1627
+ const bTime = Date.parse(b.updated_at);
1628
+ if (Number.isNaN(aTime) || Number.isNaN(bTime)) {
1629
+ return 0;
1630
+ }
1631
+ return bTime - aTime;
1632
+ });
1633
+ if (defaultSessionId) {
1634
+ const lastIndex = remoteSessions.findIndex((session) => session.id === defaultSessionId);
1635
+ if (lastIndex >= 0) {
1636
+ setHighlightIndex(lastIndex);
1637
+ }
1638
+ }
1639
+ setSessions(remoteSessions);
1640
+ setStatusHint("");
1641
+ setReady(true);
1642
+ return;
1643
+ } catch (err) {
1644
+ if (!isActive) {
1645
+ return;
1646
+ }
1647
+ const message = err instanceof Error ? err.message : "Could not load agent sessions.";
1648
+ setError(message);
1649
+ setStatusHint("");
1650
+ setSessions([]);
1651
+ setReady(true);
1652
+ }
1653
+ };
1654
+ void loadSessions();
1655
+
1656
+ return () => {
1657
+ isActive = false;
1658
+ };
1659
+ }, []);
1660
+
1661
+ const normalizeSessionLabel = (session: {
1662
+ id: string;
1663
+ title: string;
1664
+ is_archived: boolean;
1665
+ updated_at: string;
1666
+ }) => `${session.title || "(untitled)"} ${session.is_archived ? "[archived] " : ""}(updated ${session.updated_at})`;
1667
+
1668
+ const matchingSessions = sessions.filter((session) => {
1669
+ const searchable = `${session.title} ${session.id}`.toLowerCase();
1670
+ const normalizedQuery = searchQuery.trim().toLowerCase();
1671
+ return normalizedQuery.length === 0 || searchable.includes(normalizedQuery);
1672
+ });
1673
+
1674
+ useInput((_, key) => {
1675
+ if (!matchingSessions.length) return;
1676
+ if (key.upArrow) {
1677
+ setHighlightIndex((idx) => (idx === 0 ? matchingSessions.length - 1 : idx - 1));
1678
+ return;
1679
+ }
1680
+ if (key.downArrow) {
1681
+ setHighlightIndex((idx) => (idx === matchingSessions.length - 1 ? 0 : idx + 1));
1682
+ return;
1683
+ }
1684
+ if (key.return) {
1685
+ const selected = matchingSessions[highlightIndex];
1686
+ if (!selected) {
1687
+ setError("No matching sessions found.");
1688
+ return;
1689
+ }
1690
+ setError("");
1691
+ setSelectedSession({ id: selected.id });
1692
+ }
1693
+ }, { isActive: isRawModeSupported });
1694
+
1695
+ if (!isRawModeSupported) {
1696
+ return (
1697
+ <Box flexDirection="column">
1698
+ <ErrorMessage error="Interactive input is not supported in this terminal." />
1699
+ <Text>Use interactive mode for this environment:</Text>
1700
+ <Text dimColor> agent --help</Text>
1701
+ </Box>
1702
+ );
1703
+ }
1704
+
1705
+ if (error && !sessions.length) {
1706
+ return <ErrorMessage error={error} />;
1707
+ }
1708
+
1709
+ if (!isReady) {
1710
+ return <Loading message={statusHint || "Loading sessions..."} />;
1711
+ }
1712
+
1713
+ if (!sessions.length) {
1714
+ return (
1715
+ <ErrorMessage
1716
+ error="No agent sessions found. Start a new conversation with `pioneer agent` and try again."
1717
+ />
1718
+ );
1719
+ }
1720
+
1721
+ if (selectedSession?.id) {
1722
+ if (isSessionLoading) {
1723
+ return <Loading message={`Loading conversation ${selectedSession.id}...`} />;
1724
+ }
1725
+ if (sessionError) {
1726
+ return <ErrorMessage error={sessionError} />;
1727
+ }
1728
+
1729
+ return (
1730
+ <AutoAgentInteractiveSession
1731
+ conversationId={selectedSession.id}
1732
+ mode={mode ?? "standard"}
1733
+ history={selectedSessionHistory}
1734
+ allowSessionCreation={false}
1735
+ />
1736
+ );
1737
+ }
1738
+
1739
+ return (
1740
+ <Box flexDirection="column">
1741
+ <Text bold>Agent sessions:</Text>
1742
+ <Text>
1743
+ {defaultSessionId ? `Last session: ${defaultSessionId}. ` : ""}
1744
+ Type to filter. Use ↑/↓ to navigate and Enter to select.
1745
+ </Text>
1746
+ <Text dimColor>Search title or session ID.</Text>
1747
+ <Text> </Text>
1748
+ {matchingSessions.length === 0 ? (
1749
+ <Text color="yellow">No sessions match "{searchQuery}".</Text>
1750
+ ) : (
1751
+ matchingSessions.slice(0, 12).map((session, index) => {
1752
+ const isSelected = index === highlightIndex;
1753
+ return (
1754
+ <Text key={session.id} color={isSelected ? "cyan" : undefined}>
1755
+ {`${isSelected ? ">" : " "} ${normalizeSessionLabel(session)} (${session.id})`}
1756
+ </Text>
1757
+ );
1758
+ })
1759
+ )}
1760
+ <Box marginTop={1}>
1761
+ <Text color="cyan">&gt; </Text>
1762
+ <TextInput
1763
+ value={searchQuery}
1764
+ onChange={setSearchQuery}
1765
+ onSubmit={(rawValue) => {
1766
+ const trimmed = rawValue.trim().toLowerCase();
1767
+ if (!trimmed && !matchingSessions.length) {
1768
+ return;
1769
+ }
1770
+ const exactMatch = matchingSessions.find(
1771
+ (session) =>
1772
+ session.id.toLowerCase() === trimmed ||
1773
+ session.title.toLowerCase() === trimmed
1774
+ );
1775
+ const selected = exactMatch ?? matchingSessions[highlightIndex];
1776
+ if (!selected?.id) {
1777
+ setError("No matching session found.");
1778
+ return;
1779
+ }
1780
+ setError("");
1781
+ setSelectedSession({ id: selected.id });
1782
+ }}
1783
+ />
1784
+ </Box>
1785
+ {error && <ErrorMessage error={error} />}
1786
+ </Box>
1787
+ );
1788
+ }
1789
+
1212
1790
  function AgentInteractivePrompt({
1213
1791
  conversationId,
1214
1792
  history,
1215
1793
  mode,
1794
+ allowSessionCreation = true,
1216
1795
  }: {
1217
1796
  conversationId?: string;
1218
1797
  history?: api.AgentChatHistoryItem[];
1219
1798
  mode?: "standard" | "research";
1799
+ allowSessionCreation?: boolean;
1220
1800
  }) {
1221
1801
  const [input, setInput] = useState("");
1802
+ const [inputHistory, setInputHistory] = useState<string[]>([]);
1803
+ const [inputHistoryIndex, setInputHistoryIndex] = useState(-1);
1222
1804
  const [isReady, setReady] = useState(false);
1223
1805
  const [message, setMessage] = useState("");
1224
1806
  const { isRawModeSupported } = useStdin();
1225
1807
  const shouldExitImmediately = !isRawModeSupported;
1226
1808
 
1809
+ const commitInputToHistory = (value: string) => {
1810
+ const normalized = value.trim();
1811
+ if (!normalized) {
1812
+ return;
1813
+ }
1814
+ setInputHistory((previous) => {
1815
+ if (previous.length > 0 && previous[previous.length - 1] === normalized) {
1816
+ return previous;
1817
+ }
1818
+ return [...previous, normalized];
1819
+ });
1820
+ };
1821
+
1822
+ const setInputValue = (nextValue: string) => {
1823
+ setInput(nextValue);
1824
+ setInputHistoryIndex(-1);
1825
+ };
1826
+
1827
+ useInput(
1828
+ (character, key) => {
1829
+ if (key.return) {
1830
+ const trimmed = input.trim();
1831
+ if (!trimmed) {
1832
+ return;
1833
+ }
1834
+ commitInputToHistory(trimmed);
1835
+ setMessage(trimmed);
1836
+ setReady(true);
1837
+ return;
1838
+ }
1839
+
1840
+ if (key.backspace || key.delete) {
1841
+ setInputValue(input.slice(0, -1));
1842
+ return;
1843
+ }
1844
+
1845
+ if (key.upArrow) {
1846
+ if (!inputHistory.length) {
1847
+ return;
1848
+ }
1849
+ const nextIndex =
1850
+ inputHistoryIndex === -1 ? inputHistory.length - 1 : Math.max(0, inputHistoryIndex - 1);
1851
+ setInputHistoryIndex(nextIndex);
1852
+ setInput(inputHistory[nextIndex] ?? "");
1853
+ return;
1854
+ }
1855
+
1856
+ if (key.downArrow) {
1857
+ if (inputHistoryIndex === -1) {
1858
+ return;
1859
+ }
1860
+ const nextIndex = inputHistoryIndex + 1;
1861
+ if (nextIndex >= inputHistory.length) {
1862
+ setInputHistoryIndex(-1);
1863
+ setInput("");
1864
+ return;
1865
+ }
1866
+ setInputHistoryIndex(nextIndex);
1867
+ setInput(inputHistory[nextIndex] ?? "");
1868
+ return;
1869
+ }
1870
+
1871
+ if (
1872
+ key.tab ||
1873
+ key.leftArrow ||
1874
+ key.rightArrow ||
1875
+ key.escape ||
1876
+ key.ctrl ||
1877
+ key.meta
1878
+ ) {
1879
+ return;
1880
+ }
1881
+
1882
+ if (character) {
1883
+ setInputValue(input + character);
1884
+ }
1885
+ },
1886
+ { isActive: isRawModeSupported && !isReady }
1887
+ );
1888
+
1227
1889
  useEffect(() => {
1228
1890
  if (!shouldExitImmediately) {
1229
1891
  return;
@@ -1256,15 +1918,9 @@ function AgentInteractivePrompt({
1256
1918
  <Text color="cyan">&gt; </Text>
1257
1919
  <TextInput
1258
1920
  value={input}
1259
- onChange={setInput}
1260
- onSubmit={(value) => {
1261
- const trimmed = value.trim();
1262
- if (!trimmed) {
1263
- return;
1264
- }
1265
- setMessage(trimmed);
1266
- setReady(true);
1267
- }}
1921
+ focus={false}
1922
+ onChange={() => {}}
1923
+ onSubmit={() => {}}
1268
1924
  />
1269
1925
  </Box>
1270
1926
  <Text dimColor>Type Ctrl+C to cancel.</Text>
@@ -1278,6 +1934,7 @@ function AgentInteractivePrompt({
1278
1934
  history={history}
1279
1935
  mode={mode === "research" ? "research" : "standard"}
1280
1936
  firstMessage={message}
1937
+ allowSessionCreation={allowSessionCreation}
1281
1938
  />
1282
1939
  );
1283
1940
  }
@@ -1304,6 +1961,7 @@ function ModelCreateInteractive({
1304
1961
  const [loading, setLoading] = useState(true);
1305
1962
  const [error, setError] = useState("");
1306
1963
  const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
1964
+ const { isRawModeSupported } = useStdin();
1307
1965
  const [highlightIndex, setHighlightIndex] = useState(0);
1308
1966
 
1309
1967
  useEffect(() => {
@@ -1352,25 +2010,28 @@ function ModelCreateInteractive({
1352
2010
  }
1353
2011
  }, [highlightIndex, topMatches.length]);
1354
2012
 
1355
- useInput((_, key) => {
1356
- if (!topMatches.length) return;
1357
- if (key.upArrow) {
1358
- setHighlightIndex((idx) => (idx === 0 ? topMatches.length - 1 : idx - 1));
1359
- return;
1360
- }
1361
- if (key.downArrow) {
1362
- setHighlightIndex((idx) => (idx === topMatches.length - 1 ? 0 : idx + 1));
1363
- return;
1364
- }
1365
- if (key.return) {
1366
- const resolvedModel = resolveModelId(query);
1367
- if (!resolvedModel) {
1368
- setError("No matching models found. Refine your search and try again.");
2013
+ useInput(
2014
+ (_, key) => {
2015
+ if (!topMatches.length) return;
2016
+ if (key.upArrow) {
2017
+ setHighlightIndex((idx) => (idx === 0 ? topMatches.length - 1 : idx - 1));
1369
2018
  return;
1370
2019
  }
1371
- setSelectedModelId(resolvedModel);
1372
- }
1373
- });
2020
+ if (key.downArrow) {
2021
+ setHighlightIndex((idx) => (idx === topMatches.length - 1 ? 0 : idx + 1));
2022
+ return;
2023
+ }
2024
+ if (key.return) {
2025
+ const resolvedModel = resolveModelId(query);
2026
+ if (!resolvedModel) {
2027
+ setError("No matching models found. Refine your search and try again.");
2028
+ return;
2029
+ }
2030
+ setSelectedModelId(resolvedModel);
2031
+ }
2032
+ },
2033
+ { isActive: isRawModeSupported }
2034
+ );
1374
2035
 
1375
2036
  if (error) {
1376
2037
  return (
@@ -2837,7 +3498,9 @@ type HelpContext =
2837
3498
  | "eval"
2838
3499
  | "benchmark"
2839
3500
  | "inference"
2840
- | "agent";
3501
+ | "agent"
3502
+ | "model-endpoints"
3503
+ | "model-artifacts";
2841
3504
 
2842
3505
  interface HelpProps {
2843
3506
  context?: HelpContext;
@@ -3188,6 +3851,8 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
3188
3851
  <Text> --mode {"<research>"} --mode research uses Pro workflow</Text>
3189
3852
  <Text> </Text>
3190
3853
  <Text> Omit --mode to use the default standard interactive mode.</Text>
3854
+ <Text> agent sessions List and resume previous sessions</Text>
3855
+ <Text> agent resume {"[conversation-id]"} List sessions, then resume a selected conversation</Text>
3191
3856
  <Text> --conversation-id {"<id>"} Continue an existing conversation</Text>
3192
3857
  <Text> --filters {"<json>"} Reserved for future query filters</Text>
3193
3858
  <Text> --history {"<json>"} Optional message history JSON</Text>
@@ -3196,6 +3861,8 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
3196
3861
  <Text dimColor>Example: pioneer agent --mode research</Text>
3197
3862
  <Text dimColor>Then type: Analyze failures and propose retraining plan</Text>
3198
3863
  <Text dimColor>Then type: Draft a short status summary</Text>
3864
+ <Text dimColor>Example: pioneer agent resume</Text>
3865
+ <Text dimColor>Example: pioneer agent resume b042f7a1-0e7e-4f78-96df-a1cc2d4afcdf</Text>
3199
3866
  </Box>
3200
3867
  );
3201
3868
  }
@@ -3269,6 +3936,45 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
3269
3936
  }
3270
3937
 
3271
3938
  if (hasParseErrors && !isModelCreateMissingModel) {
3939
+ const missingValueHints: Record<string, string> = {
3940
+ "--model": "<base-model-id>",
3941
+ "--mode":
3942
+ group === "agent" || group === "agents"
3943
+ ? "<research>"
3944
+ : "<value>",
3945
+ "--conversation-id": "<session-id>",
3946
+ "--conversation": "<session-id>",
3947
+ "--history": "<json>",
3948
+ "--filters": "<json>",
3949
+ "--format": "<format>",
3950
+ "--text": "<text>",
3951
+ "--prompt": "<text>",
3952
+ "--name": "<name>",
3953
+ "--repo": "<url>",
3954
+ "--icon": "<icon>",
3955
+ "--description": "<text>",
3956
+ "--api-key": "<key>",
3957
+ "--api-url": "<url>",
3958
+ "--message": "<text>",
3959
+ "--inputs": "<json>",
3960
+ "--labels": "<json-array>",
3961
+ "--label-column": "<column>",
3962
+ "--text-column": "<column>",
3963
+ "--dataset-ids": "<comma-separated-ids>",
3964
+ "--output": "<path>",
3965
+ "--format-results": "<true|false>",
3966
+ "--include-confidence": "<true|false>",
3967
+ "--include-spans": "<true|false>",
3968
+ "--reasoning-trace": "<true|false>",
3969
+ "--reasoning-effort": "<low|medium|high>",
3970
+ };
3971
+ const getValueHint = (flag: string) => {
3972
+ if (flag === "--mode" && group === "agent") {
3973
+ return "<research> (default is standard when omitted)";
3974
+ }
3975
+ return missingValueHints[flag] ?? "<value>";
3976
+ };
3977
+
3272
3978
  if (isModelEndpointsDeployMissingJob) {
3273
3979
  const errorMessage = rest[1]
3274
3980
  ? `Training job ID required: model endpoints deploy ${rest[1]} --job <training-job-id>`
@@ -3296,7 +4002,7 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
3296
4002
  <ErrorMessage error="One or more flags are missing values. Please provide values for: " />
3297
4003
  {parseErrors.map((flag) => (
3298
4004
  <Text dimColor key={flag}>
3299
- - {flag} {"<value>"}
4005
+ - {flag} {getValueHint(flag)}
3300
4006
  </Text>
3301
4007
  ))}
3302
4008
  </Box>
@@ -4951,13 +5657,15 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
4951
5657
  return <Help context="agent" />;
4952
5658
  }
4953
5659
 
4954
- if (action && !action.startsWith("-")) {
5660
+ if (action && action !== "resume" && action !== "sessions" && !action.startsWith("-")) {
4955
5661
  return (
4956
5662
  <ErrorMessage
4957
5663
  error={
4958
5664
  'Invalid agent command syntax. Use one of:\n' +
4959
5665
  "pioneer agent\n" +
4960
- "pioneer agent --mode research"
5666
+ "pioneer agent --mode research\n" +
5667
+ "pioneer agent sessions\n" +
5668
+ "pioneer agent resume [conversation-id]"
4961
5669
  }
4962
5670
  />
4963
5671
  );
@@ -4992,11 +5700,34 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
4992
5700
  return <ErrorMessage error='--filters is not supported for /auto-agent/clarify. Omit this flag for now.' />;
4993
5701
  }
4994
5702
 
5703
+ if (action === "resume" || action === "sessions") {
5704
+ if (!isRawModeSupported) {
5705
+ return (
5706
+ <ErrorMessage
5707
+ error="Interactive input is not supported in this terminal.\nUse interactive mode for this environment: agent --help"
5708
+ />
5709
+ );
5710
+ }
5711
+ if (rest[0] || flags["conversation-id"]) {
5712
+ const resumeId = rest[0] ?? flags["conversation-id"];
5713
+ return (
5714
+ <AutoAgentInteractiveSession
5715
+ conversationId={resumeId}
5716
+ history={history}
5717
+ mode={mode}
5718
+ allowSessionCreation={false}
5719
+ />
5720
+ );
5721
+ }
5722
+ return <AgentResumeCommand mode={mode} />;
5723
+ }
5724
+
4995
5725
  return (
4996
5726
  <AgentInteractivePrompt
4997
5727
  conversationId={flags["conversation-id"]}
4998
5728
  history={history}
4999
5729
  mode={mode}
5730
+ allowSessionCreation={true}
5000
5731
  />
5001
5732
  );
5002
5733
  }
@@ -5027,4 +5758,6 @@ async function main() {
5027
5758
  await render(<App command={command} flags={flags} parseErrors={parseErrors} />).waitUntilExit();
5028
5759
  }
5029
5760
 
5030
- main();
5761
+ if (process.env.PIONEER_SKIP_AUTORUN !== "true") {
5762
+ main();
5763
+ }