praxis-agent 0.41.0 → 0.42.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 +2 -1
- package/dist/cli-runtime.js +9 -2
- package/dist/providers/deadline-provider.d.ts +16 -0
- package/dist/providers/deadline-provider.js +226 -0
- package/dist/providers/environment.d.ts +1 -0
- package/dist/providers/environment.js +12 -0
- package/dist/providers/fallback-provider.js +35 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -200,7 +200,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
200
200
|
imports, memory, skills, commands, agents, hooks, settings, MCP servers,
|
|
201
201
|
plugins, and append-only `praxis.transcript` JSONL sessions under `~/.praxis`.
|
|
202
202
|
- **Provider-neutral models** — native Anthropic Messages and OpenAI-compatible
|
|
203
|
-
streaming adapters with explicit capability checks
|
|
203
|
+
streaming adapters with explicit capability checks, metering controls, and
|
|
204
|
+
a bounded absolute deadline for every provider attempt.
|
|
204
205
|
|
|
205
206
|
Detailed feature status and executable evidence live in the
|
|
206
207
|
[parity matrix](https://github.com/Forest-Isle/Praxis/blob/main/docs/PARITY_MATRIX.md),
|
package/dist/cli-runtime.js
CHANGED
|
@@ -47,6 +47,7 @@ import { redactSensitiveText, sensitiveEnvironmentValues, } from './platform/sen
|
|
|
47
47
|
import { AnthropicCompatibleProvider } from './providers/anthropic-compatible.js';
|
|
48
48
|
import { createAnthropicPromptCachePolicyResolver } from './providers/anthropic-prompt-cache.js';
|
|
49
49
|
import { FallbackModelProvider } from './providers/fallback-provider.js';
|
|
50
|
+
import { DeadlineModelProvider } from './providers/deadline-provider.js';
|
|
50
51
|
import { OpenAICompatibleProvider } from './providers/openai-compatible.js';
|
|
51
52
|
import { parseContextEnvironment, parseProviderEnvironment, } from './providers/environment.js';
|
|
52
53
|
import { ModelPricingRegistry, usageCostUsd } from './core/usage.js';
|
|
@@ -107,7 +108,7 @@ function createProviderForModel({ apiKey, environment, provider, context, contro
|
|
|
107
108
|
? { contextWindowTokens: context.contextWindowTokens }
|
|
108
109
|
: {}),
|
|
109
110
|
};
|
|
110
|
-
|
|
111
|
+
const concrete = provider.provider === 'anthropic'
|
|
111
112
|
? new AnthropicCompatibleProvider({
|
|
112
113
|
...providerOptions,
|
|
113
114
|
promptCaching: resolvePromptCachePolicy({
|
|
@@ -126,7 +127,9 @@ function createProviderForModel({ apiKey, environment, provider, context, contro
|
|
|
126
127
|
...('anthropicVersion' in provider
|
|
127
128
|
? { anthropicVersion: provider.anthropicVersion }
|
|
128
129
|
: {}),
|
|
129
|
-
...('webSearch' in provider
|
|
130
|
+
...('webSearch' in provider
|
|
131
|
+
? { webSearch: provider.webSearch }
|
|
132
|
+
: {}),
|
|
130
133
|
})
|
|
131
134
|
: new OpenAICompatibleProvider({
|
|
132
135
|
...providerOptions,
|
|
@@ -142,6 +145,10 @@ function createProviderForModel({ apiKey, environment, provider, context, contro
|
|
|
142
145
|
},
|
|
143
146
|
}),
|
|
144
147
|
});
|
|
148
|
+
return new DeadlineModelProvider({
|
|
149
|
+
provider: concrete,
|
|
150
|
+
deadlineMs: provider.deadlineMs,
|
|
151
|
+
});
|
|
145
152
|
};
|
|
146
153
|
}
|
|
147
154
|
const HELP = `Praxis — local-first general agent
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ModelProvider, type ModelRequest, type ModelStreamEvent } from '../core/runtime.js';
|
|
2
|
+
export declare const DEFAULT_PROVIDER_DEADLINE_MS = 90000;
|
|
3
|
+
export interface DeadlineModelProviderOptions {
|
|
4
|
+
provider: ModelProvider;
|
|
5
|
+
deadlineMs?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Bounds each streamed completion while preserving the wrapped provider's API. */
|
|
8
|
+
export declare class DeadlineModelProvider implements ModelProvider {
|
|
9
|
+
private readonly provider;
|
|
10
|
+
private readonly deadlineMs;
|
|
11
|
+
readonly model?: string;
|
|
12
|
+
constructor(options: DeadlineModelProviderOptions);
|
|
13
|
+
get capabilities(): ModelProvider['capabilities'];
|
|
14
|
+
complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=deadline-provider.d.ts.map
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ModelProviderError, } from '../core/runtime.js';
|
|
2
|
+
export const DEFAULT_PROVIDER_DEADLINE_MS = 90_000;
|
|
3
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
4
|
+
function deferred() {
|
|
5
|
+
let resolve;
|
|
6
|
+
const promise = new Promise((promiseResolve) => {
|
|
7
|
+
resolve = promiseResolve;
|
|
8
|
+
});
|
|
9
|
+
return { promise, resolve };
|
|
10
|
+
}
|
|
11
|
+
/** Bounds each streamed completion while preserving the wrapped provider's API. */
|
|
12
|
+
export class DeadlineModelProvider {
|
|
13
|
+
provider;
|
|
14
|
+
deadlineMs;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
const deadlineMs = options.deadlineMs === undefined
|
|
17
|
+
? DEFAULT_PROVIDER_DEADLINE_MS
|
|
18
|
+
: options.deadlineMs;
|
|
19
|
+
if (!Number.isFinite(deadlineMs) ||
|
|
20
|
+
!Number.isInteger(deadlineMs) ||
|
|
21
|
+
deadlineMs <= 0) {
|
|
22
|
+
throw new Error('Provider deadline must be a positive integer');
|
|
23
|
+
}
|
|
24
|
+
this.provider = options.provider;
|
|
25
|
+
this.deadlineMs = deadlineMs;
|
|
26
|
+
Object.defineProperty(this, 'model', {
|
|
27
|
+
configurable: false,
|
|
28
|
+
enumerable: true,
|
|
29
|
+
get: () => this.provider.model,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
get capabilities() {
|
|
33
|
+
return this.provider.capabilities;
|
|
34
|
+
}
|
|
35
|
+
complete(request) {
|
|
36
|
+
let started = false;
|
|
37
|
+
let closed = false;
|
|
38
|
+
let interruption;
|
|
39
|
+
let controller;
|
|
40
|
+
let callerSignal;
|
|
41
|
+
let timer;
|
|
42
|
+
let deadlineStartedAt;
|
|
43
|
+
let underlying;
|
|
44
|
+
let cleanupStarted = false;
|
|
45
|
+
const interruptionDeferred = deferred();
|
|
46
|
+
const interruptionError = () => {
|
|
47
|
+
if (interruption?.kind === 'timeout') {
|
|
48
|
+
return new ModelProviderError('Provider request timed out', {
|
|
49
|
+
kind: 'timeout',
|
|
50
|
+
retryable: true,
|
|
51
|
+
cause: interruption.cause,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return new ModelProviderError('Provider request cancelled', {
|
|
55
|
+
kind: 'cancelled',
|
|
56
|
+
retryable: false,
|
|
57
|
+
cause: interruption?.cause,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const bestEffortUnderlyingReturn = () => {
|
|
61
|
+
const iterator = underlying;
|
|
62
|
+
if (!iterator || cleanupStarted)
|
|
63
|
+
return;
|
|
64
|
+
cleanupStarted = true;
|
|
65
|
+
let returnMethod;
|
|
66
|
+
try {
|
|
67
|
+
returnMethod = iterator.return;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!returnMethod)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
const result = returnMethod.call(iterator);
|
|
76
|
+
if (result && typeof result.then === 'function') {
|
|
77
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Cleanup is best effort; preserve the original completion outcome.
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const clearResources = () => {
|
|
85
|
+
if (timer !== undefined)
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
timer = undefined;
|
|
88
|
+
callerSignal?.removeEventListener('abort', onCallerAbort);
|
|
89
|
+
};
|
|
90
|
+
const finish = (outcome) => {
|
|
91
|
+
if (interruption !== undefined)
|
|
92
|
+
return;
|
|
93
|
+
interruption = outcome;
|
|
94
|
+
closed = true;
|
|
95
|
+
interruptionDeferred.resolve(outcome);
|
|
96
|
+
controller?.abort(outcome.cause);
|
|
97
|
+
clearResources();
|
|
98
|
+
if (outcome.kind !== 'return')
|
|
99
|
+
bestEffortUnderlyingReturn();
|
|
100
|
+
};
|
|
101
|
+
const onCallerAbort = () => finish({ kind: 'cancelled', cause: callerSignal?.reason });
|
|
102
|
+
const timeoutCause = new Error('Provider request timed out');
|
|
103
|
+
timeoutCause.name = 'TimeoutError';
|
|
104
|
+
const scheduleDeadline = () => {
|
|
105
|
+
if (deadlineStartedAt === undefined || closed)
|
|
106
|
+
return;
|
|
107
|
+
const elapsedMs = Math.max(0, performance.now() - deadlineStartedAt);
|
|
108
|
+
const remainingMs = this.deadlineMs - elapsedMs;
|
|
109
|
+
if (remainingMs <= 0) {
|
|
110
|
+
finish({ kind: 'timeout', cause: timeoutCause });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
timer = setTimeout(scheduleDeadline, Math.min(remainingMs, MAX_TIMER_DELAY_MS));
|
|
114
|
+
};
|
|
115
|
+
const start = () => {
|
|
116
|
+
if (started || closed)
|
|
117
|
+
return;
|
|
118
|
+
started = true;
|
|
119
|
+
controller = new AbortController();
|
|
120
|
+
callerSignal = request.signal;
|
|
121
|
+
if (callerSignal?.aborted)
|
|
122
|
+
onCallerAbort();
|
|
123
|
+
else
|
|
124
|
+
callerSignal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
125
|
+
if (interruption !== undefined)
|
|
126
|
+
return;
|
|
127
|
+
deadlineStartedAt = performance.now();
|
|
128
|
+
scheduleDeadline();
|
|
129
|
+
const iterable = this.provider.complete({
|
|
130
|
+
...request,
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
});
|
|
133
|
+
underlying = iterable[Symbol.asyncIterator]();
|
|
134
|
+
if (interruption !== undefined)
|
|
135
|
+
bestEffortUnderlyingReturn();
|
|
136
|
+
};
|
|
137
|
+
const next = async () => {
|
|
138
|
+
try {
|
|
139
|
+
start();
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (interruption !== undefined)
|
|
143
|
+
throw interruptionError();
|
|
144
|
+
closed = true;
|
|
145
|
+
clearResources();
|
|
146
|
+
bestEffortUnderlyingReturn();
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
if (interruption?.kind === 'return')
|
|
150
|
+
return { done: true, value: undefined };
|
|
151
|
+
if (interruption !== undefined)
|
|
152
|
+
throw interruptionError();
|
|
153
|
+
if (closed || !underlying)
|
|
154
|
+
return { done: true, value: undefined };
|
|
155
|
+
let nextPromise;
|
|
156
|
+
try {
|
|
157
|
+
const result = underlying.next();
|
|
158
|
+
nextPromise = Promise.resolve(result);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (interruption !== undefined)
|
|
162
|
+
throw interruptionError();
|
|
163
|
+
closed = true;
|
|
164
|
+
clearResources();
|
|
165
|
+
bestEffortUnderlyingReturn();
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
if (interruption !== undefined) {
|
|
169
|
+
void nextPromise.catch(() => undefined);
|
|
170
|
+
throw interruptionError();
|
|
171
|
+
}
|
|
172
|
+
const result = await Promise.race([
|
|
173
|
+
nextPromise.then((value) => ({ type: 'result', value }), (error) => ({ type: 'error', error })),
|
|
174
|
+
interruptionDeferred.promise.then((outcome) => ({
|
|
175
|
+
type: 'interruption',
|
|
176
|
+
outcome,
|
|
177
|
+
})),
|
|
178
|
+
]);
|
|
179
|
+
const observedInterruption = interruption ??
|
|
180
|
+
(result.type === 'interruption' ? result.outcome : undefined);
|
|
181
|
+
if (observedInterruption !== undefined) {
|
|
182
|
+
if (result.type === 'result')
|
|
183
|
+
void nextPromise.catch(() => undefined);
|
|
184
|
+
if (observedInterruption.kind === 'return')
|
|
185
|
+
return { done: true, value: undefined };
|
|
186
|
+
throw interruptionError();
|
|
187
|
+
}
|
|
188
|
+
if (result.type === 'error') {
|
|
189
|
+
closed = true;
|
|
190
|
+
clearResources();
|
|
191
|
+
bestEffortUnderlyingReturn();
|
|
192
|
+
throw result.error;
|
|
193
|
+
}
|
|
194
|
+
if (result.type === 'interruption') {
|
|
195
|
+
if (result.outcome.kind === 'return')
|
|
196
|
+
return { done: true, value: undefined };
|
|
197
|
+
throw interruptionError();
|
|
198
|
+
}
|
|
199
|
+
if (result.value.done) {
|
|
200
|
+
closed = true;
|
|
201
|
+
clearResources();
|
|
202
|
+
}
|
|
203
|
+
return result.value;
|
|
204
|
+
};
|
|
205
|
+
const returnFromConsumer = async () => {
|
|
206
|
+
if (!started) {
|
|
207
|
+
closed = true;
|
|
208
|
+
return { done: true, value: undefined };
|
|
209
|
+
}
|
|
210
|
+
if (!closed) {
|
|
211
|
+
finish({ kind: 'return', cause: undefined });
|
|
212
|
+
bestEffortUnderlyingReturn();
|
|
213
|
+
}
|
|
214
|
+
return { done: true, value: undefined };
|
|
215
|
+
};
|
|
216
|
+
const result = {
|
|
217
|
+
[Symbol.asyncIterator]() {
|
|
218
|
+
return result;
|
|
219
|
+
},
|
|
220
|
+
next,
|
|
221
|
+
return: returnFromConsumer,
|
|
222
|
+
};
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=deadline-provider.js.map
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { createAnthropicPromptCachePolicyResolver } from './anthropic-prompt-cache.js';
|
|
2
|
+
import { DEFAULT_PROVIDER_DEADLINE_MS } from './deadline-provider.js';
|
|
2
3
|
export function parseProviderEnvironment(environment) {
|
|
3
4
|
const provider = environment.PRAXIS_PROVIDER ?? 'openai';
|
|
4
5
|
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
5
6
|
throw new Error('PRAXIS_PROVIDER must be openai or anthropic');
|
|
6
7
|
}
|
|
8
|
+
const rawDeadlineMs = environment.PRAXIS_PROVIDER_DEADLINE_MS;
|
|
9
|
+
if (rawDeadlineMs !== undefined &&
|
|
10
|
+
(!/^\d+$/.test(rawDeadlineMs) ||
|
|
11
|
+
Number(rawDeadlineMs) <= 0 ||
|
|
12
|
+
!Number.isSafeInteger(Number(rawDeadlineMs)))) {
|
|
13
|
+
throw new Error('PRAXIS_PROVIDER_DEADLINE_MS must be a positive safe integer');
|
|
14
|
+
}
|
|
15
|
+
const deadlineMs = rawDeadlineMs === undefined
|
|
16
|
+
? DEFAULT_PROVIDER_DEADLINE_MS
|
|
17
|
+
: Number(rawDeadlineMs);
|
|
7
18
|
const maxOutputTokens = environment.PRAXIS_MAX_OUTPUT_TOKENS;
|
|
8
19
|
if (maxOutputTokens !== undefined &&
|
|
9
20
|
(!/^\d+$/.test(maxOutputTokens) ||
|
|
@@ -47,6 +58,7 @@ export function parseProviderEnvironment(environment) {
|
|
|
47
58
|
(provider === 'anthropic'
|
|
48
59
|
? 'https://api.anthropic.com/v1'
|
|
49
60
|
: 'https://api.openai.com/v1'),
|
|
61
|
+
deadlineMs,
|
|
50
62
|
...(maxOutputTokens === undefined
|
|
51
63
|
? {}
|
|
52
64
|
: { maxOutputTokens: Number(maxOutputTokens) }),
|
|
@@ -6,6 +6,38 @@ function retryable(error) {
|
|
|
6
6
|
error.retryable &&
|
|
7
7
|
modelProviderErrorKind(error) !== 'prompt_too_long');
|
|
8
8
|
}
|
|
9
|
+
function cancellationError(signal) {
|
|
10
|
+
return new ModelProviderError('Provider request cancelled', {
|
|
11
|
+
kind: 'cancelled',
|
|
12
|
+
retryable: false,
|
|
13
|
+
cause: signal.reason,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function throwIfAborted(signal) {
|
|
17
|
+
if (signal?.aborted)
|
|
18
|
+
throw cancellationError(signal);
|
|
19
|
+
}
|
|
20
|
+
async function waitForRetry(delayMs, signal) {
|
|
21
|
+
throwIfAborted(signal);
|
|
22
|
+
if (delayMs <= 0)
|
|
23
|
+
return;
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
const finish = (result) => {
|
|
27
|
+
if (settled)
|
|
28
|
+
return;
|
|
29
|
+
settled = true;
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
signal?.removeEventListener('abort', onAbort);
|
|
32
|
+
result();
|
|
33
|
+
};
|
|
34
|
+
const onAbort = () => finish(() => reject(cancellationError(signal)));
|
|
35
|
+
const timer = setTimeout(() => finish(resolve), delayMs);
|
|
36
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
37
|
+
if (signal?.aborted)
|
|
38
|
+
onAbort();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
9
41
|
/** Routes each complete request through Claude's retry-then-fallback policy. */
|
|
10
42
|
export class FallbackModelProvider {
|
|
11
43
|
active;
|
|
@@ -30,6 +62,7 @@ export class FallbackModelProvider {
|
|
|
30
62
|
let lastError;
|
|
31
63
|
for (const provider of this.providers()) {
|
|
32
64
|
for (let attempt = 0; attempt < MAX_ATTEMPTS_PER_MODEL; attempt += 1) {
|
|
65
|
+
throwIfAborted(request.signal);
|
|
33
66
|
this.active = provider;
|
|
34
67
|
const attemptStartedAt = performance.now();
|
|
35
68
|
try {
|
|
@@ -73,6 +106,7 @@ export class FallbackModelProvider {
|
|
|
73
106
|
return;
|
|
74
107
|
}
|
|
75
108
|
catch (error) {
|
|
109
|
+
throwIfAborted(request.signal);
|
|
76
110
|
lastError = error;
|
|
77
111
|
if (!retryable(error))
|
|
78
112
|
throw error;
|
|
@@ -89,8 +123,7 @@ export class FallbackModelProvider {
|
|
|
89
123
|
error: modelProviderErrorKind(error),
|
|
90
124
|
};
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
126
|
+
await waitForRetry(delay, request.signal);
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
}
|