@minasoft/mina-ai-router 0.1.4 → 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.
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RequestStore = void 0;
4
+ const openStatuses = new Set(["created", "sent", "waiting"]);
5
+ const archiveOnlyErrorPrefixes = [
6
+ "Archived by operator",
7
+ "Archived orphaned request",
8
+ ];
4
9
  class RequestStore {
5
10
  constructor(requests = []) {
6
11
  this.requests = new Map();
@@ -31,11 +36,141 @@ class RequestStore {
31
36
  ...current,
32
37
  ...patch,
33
38
  status,
39
+ diagnosticStatus: patch.diagnosticStatus ?? diagnosticStatusFor(status),
34
40
  updatedAt: new Date().toISOString(),
35
41
  };
36
42
  this.requests.set(id, updated);
37
43
  return updated;
38
44
  }
45
+ updateOpenStatus(id, status, patch = {}) {
46
+ const current = this.require(id);
47
+ if (!openStatuses.has(current.status)) {
48
+ return undefined;
49
+ }
50
+ return this.updateStatus(id, status, patch);
51
+ }
52
+ cancel(id, reason = "Cancelled by operator.", source = "system") {
53
+ const current = this.require(id);
54
+ this.assertActionAllowed(current, "cancel");
55
+ const now = new Date().toISOString();
56
+ return this.updateStatus(id, "cancelled", {
57
+ error: reason,
58
+ leaseStatus: "released",
59
+ leaseReleasedAt: now,
60
+ recoveryEvents: appendRecoveryEvent(current, {
61
+ at: now,
62
+ action: "cancel",
63
+ source,
64
+ message: reason,
65
+ previousLeaseStatus: current.leaseStatus,
66
+ activeRequestId: current.id,
67
+ }),
68
+ });
69
+ }
70
+ archive(id, reason, source = "system") {
71
+ const current = this.require(id);
72
+ this.assertActionAllowed(current, "archive");
73
+ const now = new Date().toISOString();
74
+ const releasesLease = current.leaseStatus === "active" || current.leaseStatus === "orphaned";
75
+ const archiveReason = current.error ?? reason;
76
+ return this.updateStatus(id, "archived", {
77
+ archivedAt: now,
78
+ archivedFromStatus: current.status,
79
+ error: archiveReason,
80
+ leaseStatus: releasesLease ? "released" : current.leaseStatus,
81
+ leaseReleasedAt: releasesLease ? now : current.leaseReleasedAt,
82
+ recoveryStatus: current.leaseStatus === "orphaned" ? "recovered" : current.recoveryStatus,
83
+ recoveredAt: current.leaseStatus === "orphaned" ? now : current.recoveredAt,
84
+ recoveryEvents: current.leaseStatus === "orphaned"
85
+ ? appendRecoveryEvent(current, {
86
+ at: now,
87
+ action: "archive",
88
+ source,
89
+ message: reason ?? "Archived orphaned request and released lease.",
90
+ previousLeaseStatus: current.leaseStatus,
91
+ activeRequestId: current.id,
92
+ })
93
+ : current.recoveryEvents,
94
+ });
95
+ }
96
+ unarchive(id) {
97
+ const current = this.require(id);
98
+ this.assertActionAllowed(current, "unarchive");
99
+ const restoredStatus = current.archivedFromStatus ?? "answered";
100
+ const shouldClearArchiveError = current.error
101
+ && restoredStatus !== "failed"
102
+ && restoredStatus !== "timeout"
103
+ && archiveOnlyErrorPrefixes.some((prefix) => current.error?.startsWith(prefix));
104
+ return this.updateStatus(id, restoredStatus, {
105
+ archivedAt: undefined,
106
+ archivedFromStatus: undefined,
107
+ error: shouldClearArchiveError ? undefined : current.error,
108
+ });
109
+ }
110
+ recordRetry(originalRequestId, retryRequestId) {
111
+ const original = this.require(originalRequestId);
112
+ return this.patch(original.id, {
113
+ retriedByRequestId: retryRequestId,
114
+ });
115
+ }
116
+ recordInterrupt(id, options) {
117
+ const current = this.require(id);
118
+ this.assertActionAllowed(current, "interrupt");
119
+ const now = new Date().toISOString();
120
+ return this.patch(current.id, {
121
+ recoveryStatus: "interrupted",
122
+ interruptedAt: now,
123
+ recoveryEvents: appendRecoveryEvent(current, {
124
+ at: now,
125
+ action: "interrupt",
126
+ source: options.source,
127
+ message: options.message ?? "Terminal interrupt sent by operator.",
128
+ previousLeaseStatus: current.leaseStatus,
129
+ activeRequestId: current.id,
130
+ terminalTarget: options.terminalTarget,
131
+ }),
132
+ });
133
+ }
134
+ markRecovered(id, source, message = "Marked recovered by operator.") {
135
+ const current = this.require(id);
136
+ this.assertActionAllowed(current, "recover");
137
+ const now = new Date().toISOString();
138
+ return this.patch(current.id, {
139
+ leaseStatus: "released",
140
+ leaseReleasedAt: now,
141
+ recoveryStatus: "recovered",
142
+ recoveredAt: now,
143
+ recoveryEvents: appendRecoveryEvent(current, {
144
+ at: now,
145
+ action: "recover",
146
+ source,
147
+ message,
148
+ previousLeaseStatus: current.leaseStatus,
149
+ activeRequestId: current.id,
150
+ }),
151
+ });
152
+ }
153
+ assertActionAllowed(request, action) {
154
+ const validActions = this.validActions(request);
155
+ if (!validActions.includes(action)) {
156
+ throw new Error(`Cannot ${action} request "${request.id}" while it is ${request.status}. Valid actions: ${validActions.join(", ") || "none"}.`);
157
+ }
158
+ }
159
+ validActions(request) {
160
+ if (request.status === "archived") {
161
+ return ["retry", "unarchive"];
162
+ }
163
+ if (request.leaseStatus === "orphaned") {
164
+ const recoveryActions = request.recoveryStatus === "interrupted"
165
+ ? ["recover"]
166
+ : ["interrupt", "recover"];
167
+ return [...recoveryActions, "retry", "archive"];
168
+ }
169
+ if (openStatuses.has(request.status)) {
170
+ return ["cancel"];
171
+ }
172
+ return ["retry", "archive"];
173
+ }
39
174
  patch(id, patch) {
40
175
  const current = this.require(id);
41
176
  const updated = {
@@ -48,3 +183,26 @@ class RequestStore {
48
183
  }
49
184
  }
50
185
  exports.RequestStore = RequestStore;
186
+ function appendRecoveryEvent(request, event) {
187
+ return [...(request.recoveryEvents ?? []), event];
188
+ }
189
+ function diagnosticStatusFor(status) {
190
+ switch (status) {
191
+ case "created":
192
+ case "sent":
193
+ case "waiting":
194
+ return "pending";
195
+ case "answered":
196
+ return "answered";
197
+ case "timeout":
198
+ return "timeout";
199
+ case "cancelled":
200
+ return "cancelled";
201
+ case "archived":
202
+ return "archived";
203
+ case "failed":
204
+ return "unknown_failure";
205
+ default:
206
+ return "unknown_failure";
207
+ }
208
+ }
@@ -1,33 +1,101 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ResponseMarkerNotFoundError = void 0;
3
+ exports.ResponseMarkerNotFoundError = exports.ResponseParseError = void 0;
4
+ exports.inspectMarkedResponse = inspectMarkedResponse;
4
5
  exports.parseMarkedResponse = parseMarkedResponse;
5
- class ResponseMarkerNotFoundError extends Error {
6
- constructor(requestId) {
7
- super(`Response markers were not found for request ${requestId}.`);
6
+ class ResponseParseError extends Error {
7
+ constructor(diagnostics) {
8
+ super(diagnostics.message);
9
+ this.diagnostics = diagnostics;
10
+ this.name = "ResponseParseError";
11
+ }
12
+ }
13
+ exports.ResponseParseError = ResponseParseError;
14
+ class ResponseMarkerNotFoundError extends ResponseParseError {
15
+ constructor(diagnostics) {
16
+ super(diagnostics);
8
17
  this.name = "ResponseMarkerNotFoundError";
9
18
  }
10
19
  }
11
20
  exports.ResponseMarkerNotFoundError = ResponseMarkerNotFoundError;
12
- function parseMarkedResponse(output, requestId) {
21
+ function inspectMarkedResponse(output, requestId) {
13
22
  const startMarker = `<<<MINA_AGENT_RESPONSE_START ${requestId}>>>`;
14
23
  const endMarker = `<<<MINA_AGENT_RESPONSE_END ${requestId}>>>`;
24
+ const startMarkerFound = output.includes(startMarker);
25
+ const endMarkerFound = output.includes(endMarker);
26
+ let candidateCount = 0;
27
+ let placeholderCount = 0;
15
28
  let end = output.lastIndexOf(endMarker);
16
29
  while (end !== -1) {
17
30
  const start = output.lastIndexOf(startMarker, end);
18
31
  if (start === -1) {
19
- throw new ResponseMarkerNotFoundError(requestId);
32
+ return failedDiagnostics({
33
+ kind: "missing_start_marker",
34
+ requestId,
35
+ message: `Response end marker was found without a matching start marker for request ${requestId}.`,
36
+ startMarkerFound,
37
+ endMarkerFound,
38
+ candidateCount,
39
+ placeholderCount,
40
+ });
20
41
  }
21
42
  const contentStart = start + startMarker.length;
22
43
  const answer = output.slice(contentStart, end).trim();
44
+ candidateCount += 1;
23
45
  if (!isPlaceholderAnswer(answer)) {
24
- return answer;
46
+ return {
47
+ ok: true,
48
+ answer,
49
+ diagnostics: {
50
+ kind: "parsed",
51
+ requestId,
52
+ message: `Parsed marker-wrapped response for request ${requestId}.`,
53
+ startMarkerFound,
54
+ endMarkerFound,
55
+ candidateCount,
56
+ placeholderCount,
57
+ answerLength: answer.length,
58
+ },
59
+ };
25
60
  }
61
+ placeholderCount += 1;
26
62
  end = output.lastIndexOf(endMarker, start - 1);
27
63
  }
28
- throw new ResponseMarkerNotFoundError(requestId);
64
+ if (placeholderCount > 0) {
65
+ return failedDiagnostics({
66
+ kind: "placeholder_only",
67
+ requestId,
68
+ message: `Response markers only contained placeholder content for request ${requestId}.`,
69
+ startMarkerFound,
70
+ endMarkerFound,
71
+ candidateCount,
72
+ placeholderCount,
73
+ });
74
+ }
75
+ return failedDiagnostics({
76
+ kind: "missing_markers",
77
+ requestId,
78
+ message: `Response markers were not found for request ${requestId}.`,
79
+ startMarkerFound,
80
+ endMarkerFound,
81
+ candidateCount,
82
+ placeholderCount,
83
+ });
84
+ }
85
+ function parseMarkedResponse(output, requestId) {
86
+ const result = inspectMarkedResponse(output, requestId);
87
+ if (result.ok) {
88
+ return result.answer;
89
+ }
90
+ throw new ResponseMarkerNotFoundError(result.diagnostics);
29
91
  }
30
92
  function isPlaceholderAnswer(answer) {
31
93
  const normalized = answer.replace(/[|\s]/g, "");
32
94
  return normalized === "..." || normalized === "[youranswer]";
33
95
  }
96
+ function failedDiagnostics(diagnostics) {
97
+ return {
98
+ ok: false,
99
+ diagnostics,
100
+ };
101
+ }