@evandu/super-pi 0.1.104 → 0.1.105
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/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +6 -6
- package/dist/client/assets/{index-DDkbPYLz.css → index-UiqTcZca.css} +1 -1
- package/dist/client/assets/index-tBfBUY_8.js +74 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/catalog-service.d.ts +9 -0
- package/dist/server/application/catalog-service.js +11 -0
- package/dist/server/application/provider-service.d.ts +63 -0
- package/dist/server/application/provider-service.js +136 -0
- package/dist/server/command-handler.d.ts +16 -1
- package/dist/server/command-handler.js +34 -1
- package/dist/server/index.js +13 -1
- package/dist/server/pi-session-pool.d.ts +19 -1
- package/dist/server/pi-session-pool.js +13 -1
- package/dist/server/pi-session.d.ts +10 -0
- package/dist/server/pi-session.js +11 -0
- package/dist/server/providers.d.ts +160 -0
- package/dist/server/providers.js +396 -0
- package/dist/server/runtime.d.ts +45 -0
- package/dist/server/runtime.js +38 -1
- package/dist/server/server-context.d.ts +3 -0
- package/dist/server/types.d.ts +72 -0
- package/package.json +4 -1
- package/dist/client/assets/index-mAu39Bra.js +0 -74
package/dist/client/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="alternate icon" href="/favicon.ico" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/favicon.svg" />
|
|
10
10
|
<title>Super Pi</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-tBfBUY_8.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-UiqTcZca.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -37,6 +37,15 @@ export declare class CatalogService {
|
|
|
37
37
|
* Broadcasts the model catalog to connected clients.
|
|
38
38
|
*/
|
|
39
39
|
broadcastCatalog(selection?: ModelSelection, conversationId?: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Broadcasts the UNFILTERED catalog, for the settings window's model panel.
|
|
42
|
+
*
|
|
43
|
+
* A SEPARATE EVENT FROM `model.catalog`, and the reason is the same one that made
|
|
44
|
+
* `model.catalog.all` a separate event in the first place: a `model.catalog` without a
|
|
45
|
+
* conversation is the unfiltered list, and a client that received it as its conversation's list
|
|
46
|
+
* would be offered the models that workspace's rules forbid. The panel gets its own event.
|
|
47
|
+
*/
|
|
48
|
+
broadcastAllCatalog(): Promise<void>;
|
|
40
49
|
setModelRules(rules: import("../types.js").ModelFilterRule[]): Promise<void>;
|
|
41
50
|
/**
|
|
42
51
|
* Reads one catalog filtered by mode at source.
|
|
@@ -90,6 +90,17 @@ export class CatalogService {
|
|
|
90
90
|
const catalog = await this.getCatalog(selection, conversationId);
|
|
91
91
|
this.broadcaster.broadcast({ type: "model.catalog", catalog, conversationId });
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Broadcasts the UNFILTERED catalog, for the settings window's model panel.
|
|
95
|
+
*
|
|
96
|
+
* A SEPARATE EVENT FROM `model.catalog`, and the reason is the same one that made
|
|
97
|
+
* `model.catalog.all` a separate event in the first place: a `model.catalog` without a
|
|
98
|
+
* conversation is the unfiltered list, and a client that received it as its conversation's list
|
|
99
|
+
* would be offered the models that workspace's rules forbid. The panel gets its own event.
|
|
100
|
+
*/
|
|
101
|
+
async broadcastAllCatalog() {
|
|
102
|
+
this.broadcaster.broadcast({ type: "model.catalog.all", catalog: await this.getAllCatalog() });
|
|
103
|
+
}
|
|
93
104
|
async setModelRules(rules) {
|
|
94
105
|
if (!this.store)
|
|
95
106
|
throw new Error("Store unavailable");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ProviderRegistry } from "../providers.js";
|
|
2
|
+
import type { AgentRuntime } from "../runtime.js";
|
|
3
|
+
import type { Store } from "../store.js";
|
|
4
|
+
import type { PublicProvider } from "../types.js";
|
|
5
|
+
import type { EventBroadcaster } from "../server-context.js";
|
|
6
|
+
import type { CatalogService } from "./catalog-service.js";
|
|
7
|
+
/**
|
|
8
|
+
* THE PROVIDERS A PERSON ADDED, as a service.
|
|
9
|
+
*
|
|
10
|
+
* The service owns the sequence, and the sequence is the whole point: a provider is not "saved"
|
|
11
|
+
* when it is in Super Pi's registry, it is saved when Pi can use it. So every change goes through
|
|
12
|
+
* the same four steps, in this order:
|
|
13
|
+
*
|
|
14
|
+
* 1. WRITE PI'S FILES. The definitions go into Pi's `models.json` and the key into Pi's
|
|
15
|
+
* `auth.json`, merged into whatever is already there.
|
|
16
|
+
* 2. WRITE SUPER PI'S REGISTRY. It is the truth source, and it is written AFTER Pi's files so
|
|
17
|
+
* that a failure to write them leaves Super Pi claiming nothing it has not done.
|
|
18
|
+
* 3. INVALIDATE THE CATALOG. Pi's model list is cached - the SDK's is remembered for the life of
|
|
19
|
+
* the process and the runtime's has a ten-minute TTL - so a provider added now would not
|
|
20
|
+
* appear in the picker until the cache expired. Ten minutes of a picker that cannot see the
|
|
21
|
+
* model the person just added is exactly the "decorative control" this project refuses to ship.
|
|
22
|
+
* 4. INVALIDATE THE LIVE SESSIONS. A session holds its own `ModelRuntime`, created when the
|
|
23
|
+
* session was built, and that runtime does not know a provider added a minute ago. Pi's
|
|
24
|
+
* refusal to switch to an unknown model is swallowed by the session pool into a warning - the
|
|
25
|
+
* turn then runs on the OLD model while the picker says the new one. So the sessions are made
|
|
26
|
+
* stale instead, and each one is rebuilt at its next turn, which is a moment when throwing the
|
|
27
|
+
* session away costs nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare class ProviderService {
|
|
30
|
+
private readonly registry;
|
|
31
|
+
private readonly runtime;
|
|
32
|
+
private readonly broadcaster;
|
|
33
|
+
private readonly catalogService;
|
|
34
|
+
private readonly store;
|
|
35
|
+
constructor(registry: ProviderRegistry, runtime: AgentRuntime, broadcaster: EventBroadcaster, catalogService: CatalogService, store: Store);
|
|
36
|
+
/** The providers Super Pi manages, as the browser may see them: no keys, only the fact of one. */
|
|
37
|
+
list(): Promise<PublicProvider[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Adds a provider, or adds a model to one, or updates one.
|
|
40
|
+
*
|
|
41
|
+
* ONE CALL, THREE THINGS, because the form collects a provider and a model together and a person
|
|
42
|
+
* saving a second model under the same provider should not have to retype the API address. The
|
|
43
|
+
* merge is by id: the provider's fields are replaced by what the form said, and the model with
|
|
44
|
+
* the same id is replaced while the provider's other models stay.
|
|
45
|
+
*
|
|
46
|
+
* THE KEY IS LEFT ALONE UNLESS THE FORM SAYS OTHERWISE. An empty key field means "I did not type
|
|
47
|
+
* one", not "remove the key": the key is never shown again once it is stored, so an empty field
|
|
48
|
+
* is the normal state of a form for a provider that already has one. `clearKey` is how a person
|
|
49
|
+
* asks for it to go.
|
|
50
|
+
*/
|
|
51
|
+
set(input: unknown): Promise<PublicProvider[]>;
|
|
52
|
+
/** Removes a provider Super Pi manages, and only one it manages. */
|
|
53
|
+
remove(providerId: string): Promise<PublicProvider[]>;
|
|
54
|
+
/**
|
|
55
|
+
* What has to happen after the files are written, for the change to be REAL rather than pending.
|
|
56
|
+
*
|
|
57
|
+
* The catalog is re-read for every live conversation - the same loop the model rules use - rather
|
|
58
|
+
* than broadcast once without a conversation, because a catalog without a conversation is the
|
|
59
|
+
* UNFILTERED one, and handing that to a conversation that has model rules would offer it models
|
|
60
|
+
* those rules forbid.
|
|
61
|
+
*/
|
|
62
|
+
private afterChange;
|
|
63
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { KNOWN_APIS, publicProviders, validateProvider } from "../providers.js";
|
|
2
|
+
/**
|
|
3
|
+
* THE PROVIDERS A PERSON ADDED, as a service.
|
|
4
|
+
*
|
|
5
|
+
* The service owns the sequence, and the sequence is the whole point: a provider is not "saved"
|
|
6
|
+
* when it is in Super Pi's registry, it is saved when Pi can use it. So every change goes through
|
|
7
|
+
* the same four steps, in this order:
|
|
8
|
+
*
|
|
9
|
+
* 1. WRITE PI'S FILES. The definitions go into Pi's `models.json` and the key into Pi's
|
|
10
|
+
* `auth.json`, merged into whatever is already there.
|
|
11
|
+
* 2. WRITE SUPER PI'S REGISTRY. It is the truth source, and it is written AFTER Pi's files so
|
|
12
|
+
* that a failure to write them leaves Super Pi claiming nothing it has not done.
|
|
13
|
+
* 3. INVALIDATE THE CATALOG. Pi's model list is cached - the SDK's is remembered for the life of
|
|
14
|
+
* the process and the runtime's has a ten-minute TTL - so a provider added now would not
|
|
15
|
+
* appear in the picker until the cache expired. Ten minutes of a picker that cannot see the
|
|
16
|
+
* model the person just added is exactly the "decorative control" this project refuses to ship.
|
|
17
|
+
* 4. INVALIDATE THE LIVE SESSIONS. A session holds its own `ModelRuntime`, created when the
|
|
18
|
+
* session was built, and that runtime does not know a provider added a minute ago. Pi's
|
|
19
|
+
* refusal to switch to an unknown model is swallowed by the session pool into a warning - the
|
|
20
|
+
* turn then runs on the OLD model while the picker says the new one. So the sessions are made
|
|
21
|
+
* stale instead, and each one is rebuilt at its next turn, which is a moment when throwing the
|
|
22
|
+
* session away costs nothing.
|
|
23
|
+
*/
|
|
24
|
+
export class ProviderService {
|
|
25
|
+
registry;
|
|
26
|
+
runtime;
|
|
27
|
+
broadcaster;
|
|
28
|
+
catalogService;
|
|
29
|
+
store;
|
|
30
|
+
constructor(registry, runtime, broadcaster, catalogService, store) {
|
|
31
|
+
this.registry = registry;
|
|
32
|
+
this.runtime = runtime;
|
|
33
|
+
this.broadcaster = broadcaster;
|
|
34
|
+
this.catalogService = catalogService;
|
|
35
|
+
this.store = store;
|
|
36
|
+
}
|
|
37
|
+
/** The providers Super Pi manages, as the browser may see them: no keys, only the fact of one. */
|
|
38
|
+
async list() {
|
|
39
|
+
return publicProviders(await this.registry.load());
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Adds a provider, or adds a model to one, or updates one.
|
|
43
|
+
*
|
|
44
|
+
* ONE CALL, THREE THINGS, because the form collects a provider and a model together and a person
|
|
45
|
+
* saving a second model under the same provider should not have to retype the API address. The
|
|
46
|
+
* merge is by id: the provider's fields are replaced by what the form said, and the model with
|
|
47
|
+
* the same id is replaced while the provider's other models stay.
|
|
48
|
+
*
|
|
49
|
+
* THE KEY IS LEFT ALONE UNLESS THE FORM SAYS OTHERWISE. An empty key field means "I did not type
|
|
50
|
+
* one", not "remove the key": the key is never shown again once it is stored, so an empty field
|
|
51
|
+
* is the normal state of a form for a provider that already has one. `clearKey` is how a person
|
|
52
|
+
* asks for it to go.
|
|
53
|
+
*/
|
|
54
|
+
async set(input) {
|
|
55
|
+
const requested = validateProvider(input);
|
|
56
|
+
const raw = (input ?? {});
|
|
57
|
+
const previous = await this.registry.load();
|
|
58
|
+
const existing = previous.find((provider) => provider.id === requested.id);
|
|
59
|
+
const apiKey = requested.apiKey
|
|
60
|
+
? requested.apiKey
|
|
61
|
+
: (raw.clearKey === true ? undefined : existing?.apiKey);
|
|
62
|
+
// A MODEL WITH THE SAME ID IS REPLACED WHERE IT STANDS, not moved to the end: the list a person
|
|
63
|
+
// sees in the panel is the list they added, and re-ordering it because they edited its first
|
|
64
|
+
// entry would be a change nobody asked for. A model with a NEW id is appended.
|
|
65
|
+
const incoming = requested.models[0];
|
|
66
|
+
const models = !existing
|
|
67
|
+
? requested.models
|
|
68
|
+
: (existing.models.some((model) => model.id === incoming.id)
|
|
69
|
+
? existing.models.map((model) => (model.id === incoming.id ? incoming : model))
|
|
70
|
+
: [...existing.models, incoming]);
|
|
71
|
+
const merged = {
|
|
72
|
+
id: requested.id,
|
|
73
|
+
...(requested.name ? { name: requested.name } : {}),
|
|
74
|
+
baseUrl: requested.baseUrl,
|
|
75
|
+
api: requested.api,
|
|
76
|
+
...(apiKey ? { apiKey } : {}),
|
|
77
|
+
models,
|
|
78
|
+
};
|
|
79
|
+
const next = existing
|
|
80
|
+
? [...previous.filter((provider) => provider.id !== requested.id), merged]
|
|
81
|
+
: [...previous, merged];
|
|
82
|
+
await this.registry.applyToPi(previous, next);
|
|
83
|
+
try {
|
|
84
|
+
await this.registry.save(next);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
// The registry is the truth source, so a failure to write it means Super Pi no longer knows
|
|
88
|
+
// what it put into Pi's files. Pi's files are put back the way they were, and the failure is
|
|
89
|
+
// reported rather than swallowed: a registry that disagrees with Pi's files is a state the
|
|
90
|
+
// next write would trip over.
|
|
91
|
+
await this.registry.applyToPi(next, previous).catch(() => undefined);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
await this.afterChange();
|
|
95
|
+
return publicProviders(next);
|
|
96
|
+
}
|
|
97
|
+
/** Removes a provider Super Pi manages, and only one it manages. */
|
|
98
|
+
async remove(providerId) {
|
|
99
|
+
const previous = await this.registry.load();
|
|
100
|
+
const existing = previous.find((provider) => provider.id === providerId);
|
|
101
|
+
if (!existing)
|
|
102
|
+
throw new Error(`Super Pi 没有管理供应商「${providerId}」`);
|
|
103
|
+
const next = previous.filter((provider) => provider.id !== providerId);
|
|
104
|
+
await this.registry.applyToPi(previous, next);
|
|
105
|
+
await this.registry.save(next);
|
|
106
|
+
await this.afterChange();
|
|
107
|
+
return publicProviders(next);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* What has to happen after the files are written, for the change to be REAL rather than pending.
|
|
111
|
+
*
|
|
112
|
+
* The catalog is re-read for every live conversation - the same loop the model rules use - rather
|
|
113
|
+
* than broadcast once without a conversation, because a catalog without a conversation is the
|
|
114
|
+
* UNFILTERED one, and handing that to a conversation that has model rules would offer it models
|
|
115
|
+
* those rules forbid.
|
|
116
|
+
*/
|
|
117
|
+
async afterChange() {
|
|
118
|
+
this.runtime.invalidateCatalog?.();
|
|
119
|
+
this.runtime.bumpModelEpoch?.();
|
|
120
|
+
for (const conversation of this.store.snapshot().conversations) {
|
|
121
|
+
if (conversation.archivedAt)
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
await this.catalogService.broadcastCatalog(conversation.model, conversation.id);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.warn(`供应商已保存,刷新会话 ${conversation.id} 的模型目录失败:${String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// The settings panel's OWN list, which is unfiltered and therefore has to be asked for
|
|
131
|
+
// separately. Without this the person who just added a provider would not see their own model
|
|
132
|
+
// in the default-model dropdown until they closed the window and opened it again.
|
|
133
|
+
await this.catalogService.broadcastAllCatalog?.();
|
|
134
|
+
this.broadcaster.broadcast({ type: "providers.list", providers: await this.list(), apis: [...KNOWN_APIS] });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { ConversationService } from "./application/conversation-service.js"
|
|
|
4
4
|
import type { RunService } from "./application/run-service.js";
|
|
5
5
|
import type { ExtensionUiService } from "./application/extension-ui-service.js";
|
|
6
6
|
import type { CatalogService } from "./application/catalog-service.js";
|
|
7
|
+
import type { ProviderService } from "./application/provider-service.js";
|
|
7
8
|
import type { TerminalService } from "./terminal.js";
|
|
8
9
|
export type SocketSender = (event: ServerEvent) => void;
|
|
9
10
|
/**
|
|
@@ -17,6 +18,13 @@ export declare class CommandHandler {
|
|
|
17
18
|
private readonly catalogService;
|
|
18
19
|
/** Path to the Super Pi repository root, used to spawn the restart script. */
|
|
19
20
|
private readonly rootDir?;
|
|
21
|
+
/**
|
|
22
|
+
* The providers a person added through the settings window.
|
|
23
|
+
*
|
|
24
|
+
* Optional because the command handler is constructed by tests that do not care about providers,
|
|
25
|
+
* and a missing service has to say so rather than take the whole handler down.
|
|
26
|
+
*/
|
|
27
|
+
private readonly providerService?;
|
|
20
28
|
/**
|
|
21
29
|
* The built-in terminal service.
|
|
22
30
|
*
|
|
@@ -25,7 +33,14 @@ export declare class CommandHandler {
|
|
|
25
33
|
private readonly terminalService?;
|
|
26
34
|
constructor(workspaceService: WorkspaceService, conversationService: ConversationService, runService: RunService, extensionUiService: ExtensionUiService, catalogService: CatalogService, terminalService?: TerminalService,
|
|
27
35
|
/** Path to the Super Pi repository root, used to spawn the restart script. */
|
|
28
|
-
rootDir?: string | undefined
|
|
36
|
+
rootDir?: string | undefined,
|
|
37
|
+
/**
|
|
38
|
+
* The providers a person added through the settings window.
|
|
39
|
+
*
|
|
40
|
+
* Optional because the command handler is constructed by tests that do not care about providers,
|
|
41
|
+
* and a missing service has to say so rather than take the whole handler down.
|
|
42
|
+
*/
|
|
43
|
+
providerService?: ProviderService | undefined);
|
|
29
44
|
/**
|
|
30
45
|
* Processes a client command.
|
|
31
46
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { KNOWN_APIS } from "./providers.js";
|
|
3
4
|
import { packageUpdateArgs } from "./update.js";
|
|
4
5
|
/**
|
|
5
6
|
* Routes client commands to appropriate application services and converts results or errors.
|
|
@@ -11,6 +12,7 @@ export class CommandHandler {
|
|
|
11
12
|
extensionUiService;
|
|
12
13
|
catalogService;
|
|
13
14
|
rootDir;
|
|
15
|
+
providerService;
|
|
14
16
|
/**
|
|
15
17
|
* The built-in terminal service.
|
|
16
18
|
*
|
|
@@ -19,13 +21,21 @@ export class CommandHandler {
|
|
|
19
21
|
terminalService;
|
|
20
22
|
constructor(workspaceService, conversationService, runService, extensionUiService, catalogService, terminalService,
|
|
21
23
|
/** Path to the Super Pi repository root, used to spawn the restart script. */
|
|
22
|
-
rootDir
|
|
24
|
+
rootDir,
|
|
25
|
+
/**
|
|
26
|
+
* The providers a person added through the settings window.
|
|
27
|
+
*
|
|
28
|
+
* Optional because the command handler is constructed by tests that do not care about providers,
|
|
29
|
+
* and a missing service has to say so rather than take the whole handler down.
|
|
30
|
+
*/
|
|
31
|
+
providerService) {
|
|
23
32
|
this.workspaceService = workspaceService;
|
|
24
33
|
this.conversationService = conversationService;
|
|
25
34
|
this.runService = runService;
|
|
26
35
|
this.extensionUiService = extensionUiService;
|
|
27
36
|
this.catalogService = catalogService;
|
|
28
37
|
this.rootDir = rootDir;
|
|
38
|
+
this.providerService = providerService;
|
|
29
39
|
this.terminalService = terminalService;
|
|
30
40
|
}
|
|
31
41
|
/**
|
|
@@ -69,6 +79,29 @@ export class CommandHandler {
|
|
|
69
79
|
await this.catalogService.setDefaultModel(command.model);
|
|
70
80
|
return;
|
|
71
81
|
}
|
|
82
|
+
// THE PROVIDERS A PERSON ADDED. The list is answered to the ASKING socket only - it is a panel's
|
|
83
|
+
// private list, and broadcasting it would put a provider's API address in every other tab's state
|
|
84
|
+
// for no gain. The set and the remove broadcast the new list themselves, because everybody who
|
|
85
|
+
// has the settings window open has to see the change.
|
|
86
|
+
if (command.type === "providers.set") {
|
|
87
|
+
if (!this.providerService)
|
|
88
|
+
throw new Error("供应商功能不可用");
|
|
89
|
+
await this.providerService.set(command.provider);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (command.type === "providers.remove") {
|
|
93
|
+
if (!this.providerService)
|
|
94
|
+
throw new Error("供应商功能不可用");
|
|
95
|
+
await this.providerService.remove(command.providerId);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (command.type === "providers.fetch") {
|
|
99
|
+
if (!this.providerService)
|
|
100
|
+
throw new Error("供应商功能不可用");
|
|
101
|
+
if (sendToSocket)
|
|
102
|
+
sendToSocket({ type: "providers.list", providers: await this.providerService.list(), apis: [...KNOWN_APIS] });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
72
105
|
if (command.type === "workspace.create") {
|
|
73
106
|
return await this.workspaceService.createWorkspace(command.name, command.path);
|
|
74
107
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import { SubagentRunRecorder } from "./subagent-runs.js";
|
|
|
16
16
|
import { WorkflowEngine } from "./workflow-engine.js";
|
|
17
17
|
import { ExtensionUiService } from "./application/extension-ui-service.js";
|
|
18
18
|
import { CatalogService } from "./application/catalog-service.js";
|
|
19
|
+
import { ProviderService } from "./application/provider-service.js";
|
|
20
|
+
import { ProviderRegistry } from "./providers.js";
|
|
19
21
|
import { RunService } from "./application/run-service.js";
|
|
20
22
|
import { ConversationService } from "./application/conversation-service.js";
|
|
21
23
|
import { WorkspaceService } from "./application/workspace-service.js";
|
|
@@ -178,6 +180,15 @@ const onDirsChanged = (dirs) => {
|
|
|
178
180
|
workspaceService.setAgentsDir(dirs);
|
|
179
181
|
};
|
|
180
182
|
const catalogService = new CatalogService(runtime, broadcaster, initialAgentDirs, store, rootDir, onDirsChanged);
|
|
183
|
+
/**
|
|
184
|
+
* The providers a person added through the settings window.
|
|
185
|
+
*
|
|
186
|
+
* The registry lives in Super Pi's own data directory and holds the API keys; Pi's `models.json` and
|
|
187
|
+
* `auth.json` are written from it, so a provider added here is a provider Pi can use - in a session,
|
|
188
|
+
* in a spawned Pi, and in the terminal.
|
|
189
|
+
*/
|
|
190
|
+
const providerRegistry = new ProviderRegistry(dataDir);
|
|
191
|
+
const providerService = new ProviderService(providerRegistry, runtime, broadcaster, catalogService, store);
|
|
181
192
|
const runService = new RunService(store, runtime, subagents, broadcaster, extensionUiService, initialAgentDirs, workflows);
|
|
182
193
|
runService.setMemory(memory);
|
|
183
194
|
const conversationService = new ConversationService(store, runtime, subagents, broadcaster, catalogService, runService, extensionUiService, initialAgentDirs);
|
|
@@ -216,6 +227,7 @@ const serverContext = {
|
|
|
216
227
|
runService,
|
|
217
228
|
extensionUiService,
|
|
218
229
|
catalogService,
|
|
230
|
+
providerService,
|
|
219
231
|
execService,
|
|
220
232
|
toolRegistry,
|
|
221
233
|
toolRunner,
|
|
@@ -223,7 +235,7 @@ const serverContext = {
|
|
|
223
235
|
terminalEnabled: () => terminalService.enabled,
|
|
224
236
|
terminalDisabledReason: () => terminalDisabledReason(),
|
|
225
237
|
};
|
|
226
|
-
const commandHandler = new CommandHandler(workspaceService, conversationService, runService, extensionUiService, catalogService, terminalService, rootDir);
|
|
238
|
+
const commandHandler = new CommandHandler(workspaceService, conversationService, runService, extensionUiService, catalogService, terminalService, rootDir, providerService);
|
|
227
239
|
// ------------------------------------------------------------- Lifecycle & shutdown
|
|
228
240
|
let shutdownPromise;
|
|
229
241
|
async function closeHttpServer() {
|
|
@@ -78,6 +78,15 @@ export declare class PiSessionPool {
|
|
|
78
78
|
* knows what one looks like.
|
|
79
79
|
*/
|
|
80
80
|
private readonly mintCapability;
|
|
81
|
+
/**
|
|
82
|
+
* How many times the set of models Pi knows about has changed.
|
|
83
|
+
*
|
|
84
|
+
* Injected rather than imported for the same reason the capability minting is: the pool needs to
|
|
85
|
+
* know WHEN a session is stale, and the caller knows what changed. A provider added through the
|
|
86
|
+
* settings window makes every live session unable to reach the new model, and the next turn is
|
|
87
|
+
* where that gets fixed.
|
|
88
|
+
*/
|
|
89
|
+
private readonly modelEpoch;
|
|
81
90
|
private readonly sessions;
|
|
82
91
|
constructor(
|
|
83
92
|
/** Where Pi keeps its session files. */
|
|
@@ -104,7 +113,16 @@ export declare class PiSessionPool {
|
|
|
104
113
|
* does not have to know what a capability is - it needs to know WHEN one is needed, and the caller
|
|
105
114
|
* knows what one looks like.
|
|
106
115
|
*/
|
|
107
|
-
mintCapability?: (conversationId: string) => string
|
|
116
|
+
mintCapability?: (conversationId: string) => string,
|
|
117
|
+
/**
|
|
118
|
+
* How many times the set of models Pi knows about has changed.
|
|
119
|
+
*
|
|
120
|
+
* Injected rather than imported for the same reason the capability minting is: the pool needs to
|
|
121
|
+
* know WHEN a session is stale, and the caller knows what changed. A provider added through the
|
|
122
|
+
* settings window makes every live session unable to reach the new model, and the next turn is
|
|
123
|
+
* where that gets fixed.
|
|
124
|
+
*/
|
|
125
|
+
modelEpoch?: () => number);
|
|
108
126
|
/**
|
|
109
127
|
* The session for a conversation, built if there is none and REPLACED if the live one is stale.
|
|
110
128
|
*
|
|
@@ -48,6 +48,7 @@ export class PiSessionPool {
|
|
|
48
48
|
onSessionDestroyed;
|
|
49
49
|
factory;
|
|
50
50
|
mintCapability;
|
|
51
|
+
modelEpoch;
|
|
51
52
|
sessions = new Map();
|
|
52
53
|
constructor(
|
|
53
54
|
/** Where Pi keeps its session files. */
|
|
@@ -74,13 +75,23 @@ export class PiSessionPool {
|
|
|
74
75
|
* does not have to know what a capability is - it needs to know WHEN one is needed, and the caller
|
|
75
76
|
* knows what one looks like.
|
|
76
77
|
*/
|
|
77
|
-
mintCapability = () => ""
|
|
78
|
+
mintCapability = () => "",
|
|
79
|
+
/**
|
|
80
|
+
* How many times the set of models Pi knows about has changed.
|
|
81
|
+
*
|
|
82
|
+
* Injected rather than imported for the same reason the capability minting is: the pool needs to
|
|
83
|
+
* know WHEN a session is stale, and the caller knows what changed. A provider added through the
|
|
84
|
+
* settings window makes every live session unable to reach the new model, and the next turn is
|
|
85
|
+
* where that gets fixed.
|
|
86
|
+
*/
|
|
87
|
+
modelEpoch = () => 0) {
|
|
78
88
|
this.sessionDir = sessionDir;
|
|
79
89
|
this.repositoryRoot = repositoryRoot;
|
|
80
90
|
this.agentDir = agentDir;
|
|
81
91
|
this.onSessionDestroyed = onSessionDestroyed;
|
|
82
92
|
this.factory = factory;
|
|
83
93
|
this.mintCapability = mintCapability;
|
|
94
|
+
this.modelEpoch = modelEpoch;
|
|
84
95
|
}
|
|
85
96
|
/**
|
|
86
97
|
* The session for a conversation, built if there is none and REPLACED if the live one is stale.
|
|
@@ -227,6 +238,7 @@ export class PiSessionPool {
|
|
|
227
238
|
tools: request.tools,
|
|
228
239
|
excludeTools: request.excludeTools,
|
|
229
240
|
extensions: superPiExtensionPaths(this.repositoryRoot),
|
|
241
|
+
modelEpoch: this.modelEpoch(),
|
|
230
242
|
};
|
|
231
243
|
}
|
|
232
244
|
/**
|
|
@@ -129,6 +129,16 @@ export interface SessionFingerprint {
|
|
|
129
129
|
excludeTools: string[] | undefined;
|
|
130
130
|
/** Which of Super Pi's own extensions were mounted. */
|
|
131
131
|
extensions: string[];
|
|
132
|
+
/**
|
|
133
|
+
* How many times the set of models Pi knows about has changed.
|
|
134
|
+
*
|
|
135
|
+
* NOT THE MODEL, BUT THE WORLD THE MODEL LIVES IN. A session's `ModelRuntime` is built with it and
|
|
136
|
+
* does not learn about a provider added afterwards, so a session that keeps running on the old
|
|
137
|
+
* runtime cannot switch to a model that only exists in the new one - and Pi's refusal to switch is
|
|
138
|
+
* swallowed into a warning, which would leave the turn running a model the picker does not name.
|
|
139
|
+
* The counter makes such a session stale, and it is rebuilt at its next turn.
|
|
140
|
+
*/
|
|
141
|
+
modelEpoch: number;
|
|
132
142
|
}
|
|
133
143
|
/**
|
|
134
144
|
* Fingerprints are compared by VALUE, and this is where that comparison is spelled out.
|
|
@@ -185,6 +185,8 @@ export function sameFingerprint(a, b) {
|
|
|
185
185
|
return false;
|
|
186
186
|
if (!sameSet(a.extensions, b.extensions))
|
|
187
187
|
return false;
|
|
188
|
+
if (a.modelEpoch !== b.modelEpoch)
|
|
189
|
+
return false;
|
|
188
190
|
return true;
|
|
189
191
|
}
|
|
190
192
|
function sameSet(a, b) {
|
|
@@ -343,8 +345,17 @@ async function resolveSessionManager(sdk, creation) {
|
|
|
343
345
|
* file goes out of its way not to do for a path it can derive. The derivation is Pi's own default:
|
|
344
346
|
* `~/.pi/agent`, with the home directory taken from the environment rather than from `os.homedir()`,
|
|
345
347
|
* because on Windows the two disagree often enough to matter.
|
|
348
|
+
*
|
|
349
|
+
* `PI_CODING_AGENT_DIR` WINS, exactly as it wins in Pi (`core/config.ts:getAgentDir`). A person who
|
|
350
|
+
* set it expects Pi to read its whole configuration from there, and this function used to disagree
|
|
351
|
+
* with Pi about it: Pi's model runtime read `models.json` from that directory while the session's
|
|
352
|
+
* resource loader read the skills from `~/.pi/agent`. Two halves of one Pi pointed at two
|
|
353
|
+
* directories, and only one of them was the one the person had configured.
|
|
346
354
|
*/
|
|
347
355
|
function defaultAgentDir() {
|
|
356
|
+
const configured = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
357
|
+
if (configured)
|
|
358
|
+
return configured;
|
|
348
359
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
349
360
|
return home ? join(home, ".pi", "agent") : join(".pi", "agent");
|
|
350
361
|
}
|