@buyong/pi-devin 0.1.5 → 0.1.6
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.ko.md +3 -5
- package/README.md +3 -5
- package/index.ts +6 -15
- package/package.json +3 -1
- package/src/core/auth/access.ts +7 -3
- package/src/core/auth/callback-server.ts +57 -0
- package/src/core/auth/index.ts +2 -1
- package/src/core/auth/login.ts +104 -151
- package/src/core/auth/pkce.ts +32 -0
- package/src/core/auth/rest-exchange.ts +32 -30
- package/src/core/cancellation.ts +99 -0
- package/src/core/client.ts +127 -122
- package/src/core/errors.ts +115 -0
- package/src/core/index.ts +18 -4
- package/src/core/inference/accumulator.ts +174 -0
- package/src/core/inference/images.ts +22 -15
- package/src/core/inference/request.ts +59 -0
- package/src/core/inference/retry.ts +31 -31
- package/src/core/inference/state.ts +7 -3
- package/src/core/inference/stream-chat.ts +48 -0
- package/src/core/models/access.ts +4 -3
- package/src/core/models/catalog.ts +29 -20
- package/src/core/models/select.ts +14 -12
- package/src/core/models/variants.ts +85 -72
- package/src/core/protocol/connect-client.ts +288 -0
- package/src/core/protocol/device.ts +33 -34
- package/src/core/protocol/endpoint.ts +22 -0
- package/src/core/protocol/fingerprint.ts +38 -26
- package/src/core/protocol/frames.ts +101 -0
- package/src/core/protocol/http/fetch-client.ts +42 -0
- package/src/core/protocol/http/node-client.ts +130 -0
- package/src/core/protocol/http/response.ts +99 -0
- package/src/core/protocol/http.ts +41 -101
- package/src/core/protocol/protobuf.ts +93 -74
- package/src/core/redaction.ts +33 -0
- package/src/core/services/device-identity.ts +15 -0
- package/src/core/services/http-transport.ts +28 -0
- package/src/core/services/platform.ts +54 -0
- package/src/core/types.ts +2 -23
- package/src/core/validation.ts +25 -0
- package/src/pi/adapters/pi/devin-command.ts +82 -0
- package/src/pi/adapters/pi/fusion-command.ts +92 -0
- package/src/pi/adapters/pi/oauth.ts +14 -0
- package/src/pi/adapters/pi/provider.ts +22 -0
- package/src/pi/adapters/pi/registration.ts +89 -0
- package/src/pi/adapters/pi/response-stream.ts +164 -0
- package/src/pi/adapters/pi/sidekick-tool.ts +45 -0
- package/src/pi/adapters/pi/transcript.ts +177 -0
- package/src/pi/adapters/pi/types.ts +14 -0
- package/src/pi/{devin-panel.ts → adapters/pi/ui/devin-panel.ts} +11 -9
- package/src/pi/{fusion-picker.ts → adapters/pi/ui/fusion-picker.ts} +1 -1
- package/src/pi/adapters/pi/usage-status.ts +65 -0
- package/src/pi/adapters/storage/files.ts +66 -0
- package/src/pi/adapters/storage/json-file.ts +31 -0
- package/src/pi/app/config.ts +25 -0
- package/src/pi/app/environment.ts +30 -0
- package/src/pi/app/layer.ts +43 -0
- package/src/pi/app/runtime.ts +20 -0
- package/src/pi/app/session.ts +26 -0
- package/src/pi/features/accounts/service.ts +69 -0
- package/src/pi/features/accounts/types.ts +25 -0
- package/src/pi/features/auth/credential.ts +40 -0
- package/src/pi/features/auth/guard.ts +25 -0
- package/src/pi/features/auth/login.ts +116 -0
- package/src/pi/features/auth/state.ts +48 -0
- package/src/pi/features/auth/types.ts +36 -0
- package/src/pi/features/bounded.ts +4 -0
- package/src/pi/features/catalog/constants.ts +10 -0
- package/src/pi/features/catalog/fusion.ts +45 -0
- package/src/pi/features/catalog/ids.ts +79 -0
- package/src/pi/features/catalog/projection.ts +93 -0
- package/src/pi/features/catalog/remote.ts +15 -0
- package/src/pi/features/catalog/restore.ts +53 -0
- package/src/pi/features/catalog/service.ts +158 -0
- package/src/pi/features/catalog/types.ts +56 -0
- package/src/pi/features/clients/service.ts +47 -0
- package/src/pi/features/clients/types.ts +22 -0
- package/src/pi/features/default-scheduler.ts +12 -0
- package/src/pi/features/inference/request.ts +190 -0
- package/src/pi/features/inference/service.ts +33 -0
- package/src/pi/features/inference/types.ts +11 -0
- package/src/pi/features/lifetime/service.ts +19 -0
- package/src/pi/features/lifetime/types.ts +13 -0
- package/src/pi/features/runs/admission.ts +36 -0
- package/src/pi/features/runs/lifecycle.ts +207 -0
- package/src/pi/features/runs/service.ts +10 -0
- package/src/pi/features/runs/state.ts +37 -0
- package/src/pi/features/runs/types.ts +43 -0
- package/src/pi/features/settings/service.ts +46 -0
- package/src/pi/features/settings/types.ts +36 -0
- package/src/pi/features/sidekick/child.ts +228 -0
- package/src/pi/features/sidekick/command.ts +80 -0
- package/src/pi/features/sidekick/launcher.ts +12 -0
- package/src/pi/features/sidekick/prompts.ts +17 -0
- package/src/pi/features/sidekick/report.ts +51 -0
- package/src/pi/features/sidekick/service.ts +176 -0
- package/src/pi/features/sidekick/types.ts +99 -0
- package/src/pi/features/usage/changes.ts +20 -0
- package/src/pi/{usage.ts → features/usage/report.ts} +3 -34
- package/src/pi/features/usage/types.ts +33 -0
- package/src/core/inference/chat.ts +0 -234
- package/src/core/protocol/transport.ts +0 -513
- package/src/pi/auth.ts +0 -166
- package/src/pi/devin-command.ts +0 -86
- package/src/pi/devin-settings.ts +0 -50
- package/src/pi/extension.ts +0 -89
- package/src/pi/fusion-command.ts +0 -83
- package/src/pi/fusion-settings.ts +0 -44
- package/src/pi/lifecycle.ts +0 -217
- package/src/pi/messages.ts +0 -131
- package/src/pi/models.ts +0 -415
- package/src/pi/provider.ts +0 -278
- package/src/pi/sidekick-report.ts +0 -47
- package/src/pi/sidekick-runtime.ts +0 -300
- package/src/pi/sidekick.ts +0 -211
- package/src/pi/stream.ts +0 -124
- package/src/pi/usage-store.ts +0 -75
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Effect } from "effect";
|
|
4
5
|
import protobuf from "protobufjs";
|
|
5
|
-
import {
|
|
6
|
+
import { DevinError, toDevinError } from "../errors.ts";
|
|
7
|
+
import type { JsonObject } from "../types.ts";
|
|
8
|
+
import { failWith, object } from "../validation.ts";
|
|
6
9
|
|
|
7
10
|
export const CHAT_METHOD = "exa.api_server_pb.ApiServerService/GetChatMessage";
|
|
8
11
|
|
|
9
12
|
export interface MessageCodec {
|
|
10
|
-
encode(value: JsonObject): Uint8Array
|
|
11
|
-
decode(bytes: Uint8Array): JsonObject
|
|
13
|
+
encode(value: JsonObject): Effect.Effect<Uint8Array, DevinError>;
|
|
14
|
+
decode(bytes: Uint8Array): Effect.Effect<JsonObject, DevinError>;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
let root: protobuf.Root | undefined;
|
|
15
|
-
const camelCase = (name: string) => name.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
16
|
-
|
|
17
17
|
const PROTO_ROOT = fileURLToPath(new URL("../../../proto/", import.meta.url));
|
|
18
|
+
const camelCase = (name: string) => name.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
18
19
|
|
|
19
20
|
function protoSources(directory: string, out: string[] = []): string[] {
|
|
20
21
|
for (const entry of readdirSync(directory)) {
|
|
@@ -25,87 +26,105 @@ function protoSources(directory: string, out: string[] = []): string[] {
|
|
|
25
26
|
return out;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
let loadedRoot: protobuf.Root | undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The schema is the shipped `proto/` source tree, parsed once per process. The four definitions this
|
|
33
|
+
* package added from its own protocol analysis are marked in the sources, so nothing is patched in at
|
|
34
|
+
* runtime. keepCase keeps the declared snake_case names the DTO boundary uses.
|
|
35
|
+
*/
|
|
36
|
+
const schemaRoot: Effect.Effect<protobuf.Root, DevinError> = Effect.suspend(() => loadedRoot ? Effect.succeed(loadedRoot) : Effect.try({
|
|
37
|
+
try: () => {
|
|
38
|
+
const root = new protobuf.Root();
|
|
34
39
|
root.resolvePath = (_origin: string, target: string) => isAbsolute(target) ? target : join(PROTO_ROOT, target);
|
|
35
40
|
root.loadSync(protoSources(PROTO_ROOT), { keepCase: true });
|
|
36
41
|
root.resolveAll();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
42
|
+
loadedRoot = root;
|
|
43
|
+
return root;
|
|
44
|
+
},
|
|
45
|
+
catch: toDevinError,
|
|
46
|
+
}));
|
|
40
47
|
|
|
41
48
|
/** Converts between camelCase DTOs and the original snake_case at the boundary; user keys inside maps stay unchanged. */
|
|
42
|
-
function convertFields(type: protobuf.Type, value: JsonObject, isEncoding: boolean, path: string): JsonObject {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
converted
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
49
|
+
function convertFields(type: protobuf.Type, value: JsonObject, isEncoding: boolean, path: string): Effect.Effect<JsonObject, DevinError> {
|
|
50
|
+
return Effect.gen(function* () {
|
|
51
|
+
const result: [string, unknown][] = [];
|
|
52
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
53
|
+
if (entry === undefined || entry === null) continue;
|
|
54
|
+
const field = isEncoding ? type.fieldsArray.find((candidate) => camelCase(candidate.name) === name)
|
|
55
|
+
: Object.hasOwn(type.fields, name) ? type.fields[name] : undefined;
|
|
56
|
+
if (!field) return yield* failWith(`Field not in the protobuf schema: ${path}.${name}`);
|
|
57
|
+
const label = `${path}.${name}`;
|
|
58
|
+
const convert = (item: unknown): Effect.Effect<unknown, DevinError> => {
|
|
59
|
+
if (field.resolvedType instanceof protobuf.Type) {
|
|
60
|
+
const nested = field.resolvedType;
|
|
61
|
+
return object(item, label).pipe(Effect.flatMap((fields) => convertFields(nested, fields, isEncoding, label)));
|
|
62
|
+
}
|
|
63
|
+
// Prevents fromObject from silently turning an invalid enum into 0.
|
|
64
|
+
if (isEncoding && field.resolvedType instanceof protobuf.Enum) {
|
|
65
|
+
const values = field.resolvedType;
|
|
66
|
+
const isValid = typeof item === "string" ? Object.hasOwn(values.values, item)
|
|
67
|
+
: typeof item === "number" && Object.hasOwn(values.valuesById, item);
|
|
68
|
+
if (!isValid) return failWith(`Unknown protobuf enum: ${label}`);
|
|
69
|
+
}
|
|
70
|
+
return Effect.succeed(item);
|
|
71
|
+
};
|
|
72
|
+
let converted: unknown;
|
|
73
|
+
if (field.map) {
|
|
74
|
+
const entries = Object.entries(yield* object(entry, label));
|
|
75
|
+
converted = Object.fromEntries(yield* Effect.forEach(entries, ([key, item]) => Effect.map(convert(item), (mapped) => [key, mapped] as const)));
|
|
76
|
+
} else if (field.repeated) {
|
|
77
|
+
if (!Array.isArray(entry)) return yield* failWith(`Field requires an array: ${label}`);
|
|
78
|
+
converted = yield* Effect.forEach(entry, convert);
|
|
79
|
+
} else converted = yield* convert(entry);
|
|
80
|
+
result.push([isEncoding ? field.name : camelCase(field.name), converted]);
|
|
81
|
+
}
|
|
82
|
+
return Object.fromEntries(result);
|
|
83
|
+
});
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
function messageCodec(typeName: string): MessageCodec {
|
|
73
|
-
const
|
|
87
|
+
const messageType = Effect.flatMap(schemaRoot, (root) => Effect.try({ try: () => root.lookupType(typeName), catch: toDevinError }));
|
|
74
88
|
return {
|
|
75
|
-
encode(value) {
|
|
76
|
-
const type =
|
|
77
|
-
const
|
|
89
|
+
encode: (value) => Effect.gen(function* () {
|
|
90
|
+
const type = yield* messageType;
|
|
91
|
+
const fields = yield* convertFields(type, value, true, typeName);
|
|
92
|
+
const message = yield* Effect.try({ try: () => type.fromObject(fields), catch: toDevinError });
|
|
78
93
|
const error = type.verify(message);
|
|
79
|
-
if (error)
|
|
80
|
-
return type.encode(message).finish();
|
|
81
|
-
},
|
|
82
|
-
decode(bytes) {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} catch {
|
|
90
|
-
throw new Error(`Invalid protobuf response: ${typeName}`);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
94
|
+
if (error) return yield* failWith(`Invalid protobuf input: ${error}`);
|
|
95
|
+
return yield* Effect.try({ try: () => type.encode(message).finish(), catch: toDevinError });
|
|
96
|
+
}),
|
|
97
|
+
decode: (bytes) => Effect.flatMap(messageType, (type) => Effect.gen(function* () {
|
|
98
|
+
const decoded = yield* Effect.try({
|
|
99
|
+
try: () => type.toObject(type.decode(bytes), { longs: String, enums: String, bytes: String, defaults: false }),
|
|
100
|
+
catch: toDevinError,
|
|
101
|
+
});
|
|
102
|
+
return yield* convertFields(type, yield* object(decoded, typeName), false, typeName);
|
|
103
|
+
}).pipe(Effect.mapError(() => new DevinError({ message: `Invalid protobuf response: ${typeName}` })))),
|
|
93
104
|
};
|
|
94
105
|
}
|
|
95
106
|
|
|
96
|
-
export function unaryCodecs(method: string): { request: MessageCodec; response: MessageCodec } {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
export function unaryCodecs(method: string): Effect.Effect<{ request: MessageCodec; response: MessageCodec }, DevinError> {
|
|
108
|
+
return Effect.gen(function* () {
|
|
109
|
+
const split = method.lastIndexOf("/");
|
|
110
|
+
if (split <= 0) return yield* failWith("A valid RPC path is required.");
|
|
111
|
+
const root = yield* schemaRoot;
|
|
112
|
+
const definition = yield* Effect.try({
|
|
113
|
+
try: () => {
|
|
114
|
+
const found = root.lookupService(method.slice(0, split)).methods[method.slice(split + 1)];
|
|
115
|
+
found?.resolve();
|
|
116
|
+
return found;
|
|
117
|
+
},
|
|
118
|
+
catch: toDevinError,
|
|
119
|
+
});
|
|
120
|
+
if (!definition?.resolvedRequestType || !definition.resolvedResponseType || definition.responseStream || definition.requestStream) {
|
|
121
|
+
return yield* failWith(`Unsupported unary RPC: ${method}`);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
request: messageCodec(definition.resolvedRequestType.fullName),
|
|
125
|
+
response: messageCodec(definition.resolvedResponseType.fullName),
|
|
126
|
+
};
|
|
127
|
+
});
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
export const chatRequestCodec = messageCodec("exa.api_server_pb.GetChatMessageRequest");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Predicate } from "effect";
|
|
2
|
+
import { Cancelled, DevinError, RequestTimeout, RPCError, type DevinFailure } from "./errors.ts";
|
|
3
|
+
import type { JsonObject } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export function safeError(error: unknown, secrets: readonly string[] = []): string {
|
|
6
|
+
let text = error instanceof Error ? error.message : String(error);
|
|
7
|
+
for (const secret of secrets) if (secret) text = text.split(secret).join("[REDACTED]");
|
|
8
|
+
return text.replace(/devin-session-token\$[^\s"'<>]+/g, "[REDACTED]")
|
|
9
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "").slice(0, 800);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function requestSecrets(body: JsonObject, token?: string): string[] {
|
|
13
|
+
const metadata = Predicate.isObject(body.metadata) ? body.metadata as JsonObject : {};
|
|
14
|
+
// f is ciphertext that contains the API token. Treat it as sensitive as the token itself.
|
|
15
|
+
const uppercaseFingerprint = Predicate.isString(metadata.f) ? metadata.f.toUpperCase() : undefined;
|
|
16
|
+
return [token, metadata.apiKey, metadata.f, uppercaseFingerprint, body.apiKey, body.modelAssignmentJwt, body.code, body.authorizationCode, body.codeVerifier]
|
|
17
|
+
.filter((value): value is string => Predicate.isString(value) && value.length > 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The same failure with tokens, codes and other secrets removed from its message. */
|
|
21
|
+
export function redactFailure(failure: DevinFailure, secrets: readonly string[]): DevinFailure {
|
|
22
|
+
switch (failure._tag) {
|
|
23
|
+
case "RPCError": return new RPCError({
|
|
24
|
+
code: safeError(failure.code, secrets), message: safeError(failure, secrets),
|
|
25
|
+
httpStatus: failure.httpStatus, retryAfterMs: failure.retryAfterMs,
|
|
26
|
+
});
|
|
27
|
+
case "DevinError": return new DevinError({
|
|
28
|
+
message: safeError(failure, secrets), code: failure.code === undefined ? undefined : safeError(failure.code, secrets),
|
|
29
|
+
});
|
|
30
|
+
case "Cancelled": return new Cancelled({ message: safeError(failure, secrets) });
|
|
31
|
+
case "RequestTimeout": return new RequestTimeout({ message: safeError(failure, secrets) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
3
|
+
import { collectDeviceIdentity, nodeDeviceSystem, type DeviceIdentity } from "../protocol/device.ts";
|
|
4
|
+
|
|
5
|
+
export interface DeviceIdentitySourceShape {
|
|
6
|
+
/** Reads the device values the request fingerprint is built from. Never fails: unreadable values become their documented empty value. */
|
|
7
|
+
readonly collect: Effect.Effect<DeviceIdentity>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Where request fingerprints get their device values. Each fingerprint provider collects once and keeps the result. */
|
|
11
|
+
export class DeviceIdentitySource extends Context.Service<DeviceIdentitySource, DeviceIdentitySourceShape>()("pi-devin/DeviceIdentitySource") {}
|
|
12
|
+
|
|
13
|
+
/** Reads this machine through the process and network APIs. */
|
|
14
|
+
export const NodeDeviceIdentitySource: Layer.Layer<DeviceIdentitySource, never, ChildProcessSpawner.ChildProcessSpawner> = Layer.effect(DeviceIdentitySource,
|
|
15
|
+
Effect.map(ChildProcessSpawner.ChildProcessSpawner, (spawner) => ({ collect: collectDeviceIdentity(nodeDeviceSystem(spawner)) })));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Context, Effect, Layer, type Scope } from "effect";
|
|
2
|
+
import type { HttpClient } from "effect/http";
|
|
3
|
+
import { makeFetchHttpClient } from "../protocol/http/fetch-client.ts";
|
|
4
|
+
import { makeNodeHttpClient, type HTTPTransportOptions } from "../protocol/http/node-client.ts";
|
|
5
|
+
|
|
6
|
+
export interface HttpTransportShape {
|
|
7
|
+
/**
|
|
8
|
+
* An `HttpClient` for the calling scope. The built-in transport opens its own keep-alive pool and destroys it with the scope,
|
|
9
|
+
* so clients never share connections and closing one never drops another's.
|
|
10
|
+
*/
|
|
11
|
+
readonly open: Effect.Effect<HttpClient.HttpClient, never, Scope.Scope>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** How Devin clients and logins reach the network. */
|
|
15
|
+
export class HttpTransport extends Context.Service<HttpTransport, HttpTransportShape>()("pi-devin/HttpTransport") {}
|
|
16
|
+
|
|
17
|
+
/** Node's `http`/`https`: TLS 1.2 or later with certificate and host checks, HTTP/1.1 only, and a pool per opening scope. */
|
|
18
|
+
export function nodeTransport(options: HTTPTransportOptions = {}): HttpTransportShape {
|
|
19
|
+
return { open: makeNodeHttpClient(options) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** An injected `fetch`, such as the one Pi passes in stream options. */
|
|
23
|
+
export function fetchTransport(fetchImpl: typeof fetch): HttpTransportShape {
|
|
24
|
+
return { open: Effect.sync(() => makeFetchHttpClient(fetchImpl)) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const NodeHttpTransport = (options?: HTTPTransportOptions): Layer.Layer<HttpTransport> => Layer.succeed(HttpTransport, nodeTransport(options));
|
|
28
|
+
export const FetchHttpTransport = (fetchImpl: typeof fetch): Layer.Layer<HttpTransport> => Layer.succeed(HttpTransport, fetchTransport(fetchImpl));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Effect, Layer, Stream } from "effect";
|
|
2
|
+
import type { PlatformError } from "effect/PlatformError";
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
|
+
// Subpath imports: the package index also loads NodeRedis, whose `redis` peer Pi's installer does not install.
|
|
5
|
+
import * as NodeChildProcessSpawner from "@effect/platform-node/NodeChildProcessSpawner";
|
|
6
|
+
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
|
|
7
|
+
import * as NodePath from "@effect/platform-node/NodePath";
|
|
8
|
+
import { DevinError, toDevinError } from "../errors.ts";
|
|
9
|
+
|
|
10
|
+
/** Keeps the operating system's own message and error code, as the Node APIs reported them. */
|
|
11
|
+
export function fromPlatformError(error: PlatformError): DevinError {
|
|
12
|
+
const cause = error.reason.cause;
|
|
13
|
+
return cause === undefined ? new DevinError({ message: error.message }) : toDevinError(cause);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Node implementations of the process and file services this package uses. */
|
|
17
|
+
export const NodePlatformLayer = Layer.mergeAll(
|
|
18
|
+
NodeChildProcessSpawner.layer.pipe(Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))),
|
|
19
|
+
NodeFileSystem.layer,
|
|
20
|
+
NodePath.layer,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export interface CommandOutputOptions {
|
|
24
|
+
env: Record<string, string>;
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
maxOutputBytes: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Runs a fixed command without a shell input and returns its stdout, like `execFileSync` with an output bound. */
|
|
30
|
+
export function commandOutput(file: string, args: readonly string[], options: CommandOutputOptions): Effect.Effect<string, DevinError, ChildProcessSpawner.ChildProcessSpawner> {
|
|
31
|
+
return Effect.scoped(Effect.gen(function* () {
|
|
32
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
33
|
+
const handle = yield* spawner.spawn(ChildProcess.make(file, [...args], {
|
|
34
|
+
env: options.env, extendEnv: false, detached: false, stdin: "ignore", stderr: "ignore",
|
|
35
|
+
})).pipe(Effect.mapError(fromPlatformError));
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
const chunks: Uint8Array[] = [];
|
|
38
|
+
yield* handle.stdout.pipe(
|
|
39
|
+
Stream.mapError(fromPlatformError),
|
|
40
|
+
Stream.runForEach((chunk) => {
|
|
41
|
+
bytes += chunk.byteLength;
|
|
42
|
+
if (bytes > options.maxOutputBytes) return Effect.fail(new DevinError({ message: `${file} output exceeds ${options.maxOutputBytes} bytes.` }));
|
|
43
|
+
chunks.push(chunk);
|
|
44
|
+
return Effect.void;
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
const exitCode = yield* handle.exitCode.pipe(Effect.mapError(fromPlatformError));
|
|
48
|
+
if (exitCode !== 0) return yield* Effect.fail(new DevinError({ message: `${file} exited with code ${exitCode}.` }));
|
|
49
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
50
|
+
})).pipe(Effect.timeoutOrElse({
|
|
51
|
+
duration: options.timeoutMs,
|
|
52
|
+
orElse: () => Effect.fail(new DevinError({ message: `${file} timed out after ${options.timeoutMs}ms.`, code: "ETIMEDOUT" })),
|
|
53
|
+
}));
|
|
54
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
export type JsonObject = Record<string, unknown>;
|
|
2
2
|
|
|
3
|
-
export function object(value: unknown, label: string): JsonObject {
|
|
4
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5
|
-
throw new Error(`${label}: a JSON object is required.`);
|
|
6
|
-
}
|
|
7
|
-
return value as JsonObject;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function requiredString(value: unknown, label: string): string {
|
|
11
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
12
|
-
throw new Error(`${label}: a non-empty string is required.`);
|
|
13
|
-
}
|
|
14
|
-
return value;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function tokenCount(value: unknown, label: string): number {
|
|
18
|
-
if (typeof value !== "number" && !(typeof value === "string" && /^\d+$/.test(value))) {
|
|
19
|
-
throw new Error(`${label}: invalid token count.`);
|
|
20
|
-
}
|
|
21
|
-
const count = Number(value);
|
|
22
|
-
if (!Number.isSafeInteger(count) || count < 0) throw new Error(`${label}: invalid token count.`);
|
|
23
|
-
return count;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
3
|
export interface Credential {
|
|
27
4
|
version: 1;
|
|
28
5
|
sessionToken: string;
|
|
@@ -87,6 +64,8 @@ export interface ChatResult {
|
|
|
87
64
|
stopFrameIndex?: number;
|
|
88
65
|
lastUsageFrameIndex?: number;
|
|
89
66
|
idlessToolDeltaCount: number;
|
|
67
|
+
/** Tool call ID → why its arguments cannot run, in Pi's `argumentsError` wording. */
|
|
68
|
+
toolArgumentErrors: Readonly<Record<string, string>>;
|
|
90
69
|
durationMs: number;
|
|
91
70
|
firstDeltaMs?: number;
|
|
92
71
|
actualModelUid?: string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect, Predicate } from "effect";
|
|
2
|
+
import { DevinError } from "./errors.ts";
|
|
3
|
+
import type { JsonObject } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export function object(value: unknown, label: string): Effect.Effect<JsonObject, DevinError> {
|
|
6
|
+
return Predicate.isObject(value) ? Effect.succeed(value as JsonObject)
|
|
7
|
+
: Effect.fail(new DevinError({ message: `${label}: a JSON object is required.` }));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function requiredString(value: unknown, label: string): Effect.Effect<string, DevinError> {
|
|
11
|
+
return Predicate.isString(value) && value.length > 0 ? Effect.succeed(value)
|
|
12
|
+
: Effect.fail(new DevinError({ message: `${label}: a non-empty string is required.` }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function tokenCount(value: unknown, label: string): Effect.Effect<number, DevinError> {
|
|
16
|
+
const isCountLike = Predicate.isNumber(value) || (Predicate.isString(value) && /^\d+$/.test(value));
|
|
17
|
+
const count = Number(value);
|
|
18
|
+
return isCountLike && Number.isSafeInteger(count) && count >= 0 ? Effect.succeed(count)
|
|
19
|
+
: Effect.fail(new DevinError({ message: `${label}: invalid token count.` }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Fails with a message-only error. */
|
|
23
|
+
export function failWith(message: string): Effect.Effect<never, DevinError> {
|
|
24
|
+
return Effect.fail(new DevinError({ message }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { toDevinFailure, type DevinError, type DevinFailure } from "../../../core/errors.ts";
|
|
4
|
+
import { ExtensionConfig } from "../../app/config.ts";
|
|
5
|
+
import type { ExtensionRuntime } from "../../app/runtime.ts";
|
|
6
|
+
import { ModelCatalog } from "../../features/catalog/service.ts";
|
|
7
|
+
import { DisplaySettings } from "../../features/settings/service.ts";
|
|
8
|
+
import type { DevinPreferences } from "../../features/settings/types.ts";
|
|
9
|
+
import { sessionUsage, usageReport } from "../../features/usage/report.ts";
|
|
10
|
+
import { DevinPanel, PREFERENCE_ITEMS } from "./ui/devin-panel.ts";
|
|
11
|
+
import { UsageStatus } from "./usage-status.ts";
|
|
12
|
+
|
|
13
|
+
export const DEVIN_COMMAND_NAME = "devin";
|
|
14
|
+
const USAGE = "Usage: /devin, /devin recommended on|off, /devin status on|off";
|
|
15
|
+
const ARGUMENT_KEYS: Readonly<Record<string, keyof DevinPreferences>> = { recommended: "recommendedOnly", status: "showUsageStatus" };
|
|
16
|
+
|
|
17
|
+
const onOff = (value: boolean) => value ? "on" : "off";
|
|
18
|
+
|
|
19
|
+
/** Saves the settings and applies them: a changed filter rebuilds Pi's model snapshots without network. Only a failed save fails. */
|
|
20
|
+
const applyPreferences = (ctx: ExtensionCommandContext, next: DevinPreferences): Effect.Effect<void, DevinError, DisplaySettings | ModelCatalog | UsageStatus> => Effect.gen(function* () {
|
|
21
|
+
const settings = yield* DisplaySettings;
|
|
22
|
+
const catalog = yield* ModelCatalog;
|
|
23
|
+
const previous = yield* settings.read;
|
|
24
|
+
// A list refresh failure after saving is reported as a warning so the screen and the saved value stay in sync.
|
|
25
|
+
yield* settings.write(next);
|
|
26
|
+
if (previous.recommendedOnly !== next.recommendedOnly) {
|
|
27
|
+
yield* catalog.rebuild;
|
|
28
|
+
// Pi keeps model and availability snapshots. Rebuild them without network so `/model` and RPC lists update immediately.
|
|
29
|
+
yield* Effect.tryPromise({ try: () => ctx.modelRegistry.refresh({ providers: ["devin"], allowNetwork: false }), catch: toDevinFailure }).pipe(
|
|
30
|
+
Effect.catch((error) => Effect.sync(() => ctx.ui.notify(`Settings were saved, but the model list could not be rebuilt: ${error.message}`, "warning"))),
|
|
31
|
+
);
|
|
32
|
+
const model = ctx.model;
|
|
33
|
+
if (model?.provider === "devin" && !(yield* catalog.models).some((item) => item.id === model.id)) {
|
|
34
|
+
ctx.ui.notify(`The current model ${model.name} is now hidden from the list but stays in use for this session. To restore it when reopening the session, pick a model from the list.`, "warning");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
yield* (yield* UsageStatus).update(ctx);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const devinCommand = (args: string, ctx: ExtensionCommandContext, runtime: ExtensionRuntime): Effect.Effect<void, DevinFailure, DisplaySettings | ModelCatalog | UsageStatus | ExtensionConfig> => Effect.gen(function* () {
|
|
41
|
+
const settings = yield* DisplaySettings;
|
|
42
|
+
const status = yield* UsageStatus;
|
|
43
|
+
const { now } = yield* ExtensionConfig;
|
|
44
|
+
const report = () => runtime.runSync(Effect.gen(function* () {
|
|
45
|
+
return usageReport(yield* status.accountUsage, sessionUsage(ctx.sessionManager.getBranch()), now());
|
|
46
|
+
}));
|
|
47
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
48
|
+
if (tokens.length) {
|
|
49
|
+
const key = ARGUMENT_KEYS[tokens[0]];
|
|
50
|
+
if (tokens.length !== 2 || !key || (tokens[1] !== "on" && tokens[1] !== "off")) {
|
|
51
|
+
ctx.ui.notify(`Unknown argument. ${USAGE}`, "error");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const value = tokens[1] === "on";
|
|
55
|
+
yield* applyPreferences(ctx, { ...(yield* settings.read), [key]: value });
|
|
56
|
+
ctx.ui.notify(`${PREFERENCE_ITEMS.find((item) => item.key === key)!.label}: ${onOff(value)} (saved globally)`, "info");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (ctx.mode === "tui") {
|
|
60
|
+
const preferences = yield* settings.read;
|
|
61
|
+
const services = yield* Effect.context<DisplaySettings | ModelCatalog | UsageStatus>();
|
|
62
|
+
yield* Effect.tryPromise({ try: () => ctx.ui.custom<void>((tui, theme, _keybindings, done) => new DevinPanel({
|
|
63
|
+
theme, report, preferences, onChange: (next) => applyPreferences(ctx, next).pipe(Effect.provideContext(services)),
|
|
64
|
+
requestRender: () => tui.requestRender(), done: () => done(),
|
|
65
|
+
})), catch: toDevinFailure });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const preferences = yield* settings.read;
|
|
69
|
+
const summary = PREFERENCE_ITEMS.map((item) => `${item.label} ${onOff(preferences[item.key])}`).join(" · ");
|
|
70
|
+
ctx.ui.notify([...report().map((line) => line.text), `Settings: ${summary}`, USAGE].join("\n"), "info");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `/devin` shows usage details and display settings. Settings are saved globally and applied immediately.
|
|
75
|
+
* The TUI gets a dedicated screen; other modes use arguments and notifications.
|
|
76
|
+
*/
|
|
77
|
+
export function registerDevinCommand(pi: ExtensionAPI, runtime: ExtensionRuntime): void {
|
|
78
|
+
pi.registerCommand(DEVIN_COMMAND_NAME, {
|
|
79
|
+
description: "Show Devin usage and change display settings (recommended models only, usage in the status line); saved globally",
|
|
80
|
+
handler: (args, ctx) => runtime.runPromise(devinCommand(args, ctx, runtime)),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { toDevinFailure, type DevinFailure } from "../../../core/errors.ts";
|
|
4
|
+
import type { ExtensionRuntime } from "../../app/runtime.ts";
|
|
5
|
+
import { FUSION_MODEL_ID } from "../../features/catalog/constants.ts";
|
|
6
|
+
import { ModelCatalog } from "../../features/catalog/service.ts";
|
|
7
|
+
import type { DevinModel, FusionChoices, FusionSelection } from "../../features/catalog/types.ts";
|
|
8
|
+
import { FusionSettings } from "../../features/settings/service.ts";
|
|
9
|
+
import { FusionPicker } from "./ui/fusion-picker.ts";
|
|
10
|
+
|
|
11
|
+
export const FUSION_COMMAND_NAME = "devin-fusion";
|
|
12
|
+
const USAGE = `Usage: /${FUSION_COMMAND_NAME} or /${FUSION_COMMAND_NAME} <lead model ID> <sidekick model ID>`;
|
|
13
|
+
|
|
14
|
+
const host = <A>(evaluate: () => Promise<A>) => Effect.tryPromise({ try: evaluate, catch: toDevinFailure });
|
|
15
|
+
|
|
16
|
+
const currentFusion = (models: readonly DevinModel[]) => models.find((model) => model.id === FUSION_MODEL_ID);
|
|
17
|
+
|
|
18
|
+
/** Disambiguates duplicate display names in a select list with an index, and maps the chosen label back to its value. */
|
|
19
|
+
function choose<T>(ctx: ExtensionCommandContext, title: string, items: readonly T[], label: (item: T) => string, isCurrent: (item: T) => boolean): Effect.Effect<T | undefined, DevinFailure> {
|
|
20
|
+
const names = items.map(label);
|
|
21
|
+
const labels = items.map((item, index) => {
|
|
22
|
+
const name = names.filter((other) => other === names[index]).length > 1 ? `${names[index]} (${index + 1})` : names[index];
|
|
23
|
+
return (isCurrent(item) ? "● " : " ") + name;
|
|
24
|
+
});
|
|
25
|
+
return Effect.map(host(() => ctx.ui.select(title, labels)), (picked) => picked === undefined ? undefined : items[labels.indexOf(picked)]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pickInteractively(ctx: ExtensionCommandContext, choices: FusionChoices, current: FusionSelection | undefined): Effect.Effect<FusionSelection | undefined, DevinFailure> {
|
|
29
|
+
return Effect.gen(function* () {
|
|
30
|
+
const lead = yield* choose(ctx, "Devin Fusion · lead model", choices.leads, (item) => item.name, (item) => item.id === current?.leadID);
|
|
31
|
+
if (!lead) return undefined;
|
|
32
|
+
const groups = [...new Set(choices.sidekicks.map((item) => item.groupName))];
|
|
33
|
+
const currentGroup = choices.sidekicks.find((item) => item.id === current?.sidekickID)?.groupName;
|
|
34
|
+
const group = groups.length === 1 ? groups[0] : yield* choose(ctx, "Devin Fusion · sidekick model", groups, (item) => item, (item) => item === currentGroup);
|
|
35
|
+
if (group === undefined) return undefined;
|
|
36
|
+
const variants = choices.sidekicks.filter((item) => item.groupName === group);
|
|
37
|
+
const sidekick = variants.length === 1 ? variants[0]
|
|
38
|
+
: yield* choose(ctx, `Devin Fusion · sidekick variant (${group})`, variants, (item) => item.name, (item) => item.id === current?.sidekickID);
|
|
39
|
+
return sidekick ? { leadID: lead.id, sidekickID: sidekick.id } : undefined;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fusionCommand = (args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Effect.Effect<void, DevinFailure, ModelCatalog | FusionSettings> => Effect.gen(function* () {
|
|
44
|
+
const catalog = yield* ModelCatalog;
|
|
45
|
+
const settings = yield* FusionSettings;
|
|
46
|
+
const choices = yield* catalog.fusionChoices;
|
|
47
|
+
if (!choices?.leads.length || !choices.sidekicks.length) {
|
|
48
|
+
ctx.ui.notify("No Devin models are available for Fusion. Check that the model list refreshed after /login.", "warning");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const before = currentFusion(yield* catalog.models);
|
|
52
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
53
|
+
let selection: FusionSelection | undefined;
|
|
54
|
+
if (tokens.length) {
|
|
55
|
+
const [leadID, sidekickID] = tokens;
|
|
56
|
+
if (tokens.length !== 2 || !choices.leads.some((item) => item.id === leadID) || !choices.sidekicks.some((item) => item.id === sidekickID)) {
|
|
57
|
+
ctx.ui.notify(`This model cannot be used for Fusion. ${USAGE}`, "error");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
selection = { leadID, sidekickID };
|
|
61
|
+
} else if (ctx.mode === "tui") {
|
|
62
|
+
selection = yield* host(() => ctx.ui.custom<FusionSelection | undefined>((tui, theme, _keybindings, done) => new FusionPicker({
|
|
63
|
+
choices, current: before?.devin.fusion, theme, requestRender: () => tui.requestRender(), done,
|
|
64
|
+
})));
|
|
65
|
+
if (!selection) return;
|
|
66
|
+
} else if (ctx.hasUI) {
|
|
67
|
+
// RPC clients cannot draw the custom screen, so they get the select dialogs.
|
|
68
|
+
selection = yield* pickInteractively(ctx, choices, before?.devin.fusion);
|
|
69
|
+
if (!selection) return;
|
|
70
|
+
} else {
|
|
71
|
+
ctx.ui.notify(before ? `Current: ${before.name}` : `No Devin Fusion pair is set. ${USAGE}`, "info");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
yield* settings.write(selection);
|
|
75
|
+
yield* catalog.rebuild;
|
|
76
|
+
// Pi keeps model and availability snapshots. Rebuild them without network so `/model` and RPC lists update immediately.
|
|
77
|
+
yield* host(() => ctx.modelRegistry.refresh({ providers: ["devin"], allowNetwork: false }));
|
|
78
|
+
const after = currentFusion(yield* catalog.models);
|
|
79
|
+
if (after && ctx.model?.provider === "devin" && ctx.model.id === FUSION_MODEL_ID) yield* host(() => pi.setModel(after));
|
|
80
|
+
ctx.ui.notify(after ? `Saved ${after.name}. Pick Devin Fusion in /model to use it.` : "The Fusion pair was saved, but it cannot be built from the current model list.", "info");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `/devin-fusion` only chooses the lead/sidekick pair. Whether Fusion is used is decided by picking Devin Fusion in `/model`.
|
|
85
|
+
* If the current model is Fusion, it is re-selected with the updated model object so the thinking level range matches.
|
|
86
|
+
*/
|
|
87
|
+
export function registerFusionCommand(pi: ExtensionAPI, runtime: ExtensionRuntime): void {
|
|
88
|
+
pi.registerCommand(FUSION_COMMAND_NAME, {
|
|
89
|
+
description: "Choose the lead and sidekick models for Devin Fusion (saved globally)",
|
|
90
|
+
handler: (args, ctx) => runtime.runPromise(fusionCommand(args, ctx, pi)),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OAuthAuth } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ProviderRuntime } from "../../app/runtime.ts";
|
|
3
|
+
import { AccountLogin } from "../../features/auth/login.ts";
|
|
4
|
+
|
|
5
|
+
/** Pi's OAuth contract over the sign-in service. Promises reject with the original failure, so Pi sees its name and message. */
|
|
6
|
+
export function makeOAuthAuth(runtime: ProviderRuntime): OAuthAuth {
|
|
7
|
+
const accountLogin = runtime.runSync(AccountLogin);
|
|
8
|
+
return {
|
|
9
|
+
name: "Devin account",
|
|
10
|
+
login: (interaction) => runtime.runPromise(accountLogin.login(interaction)),
|
|
11
|
+
refresh: (current, signal) => runtime.runPromise(accountLogin.refresh(current, signal)),
|
|
12
|
+
toAuth: (current) => runtime.runPromise(accountLogin.toAuth(current)),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ProviderRuntime } from "../../app/runtime.ts";
|
|
2
|
+
import { ModelCatalog } from "../../features/catalog/service.ts";
|
|
3
|
+
import { Inference } from "../../features/inference/service.ts";
|
|
4
|
+
import { makeOAuthAuth } from "./oauth.ts";
|
|
5
|
+
import type { DevinProvider } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The provider Pi registers. Building it builds the runtime's layers, which perform no I/O; work starts with Pi's first refresh or request.
|
|
9
|
+
* Pi's synchronous calls run synchronous effects; `stream` returns at once while the request runs in a fiber the inference service owns.
|
|
10
|
+
*/
|
|
11
|
+
export function makeDevinProvider(runtime: ProviderRuntime): DevinProvider {
|
|
12
|
+
const catalog = runtime.runSync(ModelCatalog);
|
|
13
|
+
const inference = runtime.runSync(Inference);
|
|
14
|
+
return {
|
|
15
|
+
id: "devin", name: "Devin", auth: { oauth: makeOAuthAuth(runtime) },
|
|
16
|
+
getModels: () => runtime.runSync(catalog.models),
|
|
17
|
+
filterModels: (models, credential) => runtime.runSync(catalog.filter(models, credential)),
|
|
18
|
+
refreshModels: (context) => runtime.runPromise(catalog.refresh(context)),
|
|
19
|
+
stream: (model, context, options) => runtime.runSync(inference.start(model, context, options)),
|
|
20
|
+
streamSimple: (model, context, options) => runtime.runSync(inference.start(model, context, options)),
|
|
21
|
+
};
|
|
22
|
+
}
|