@astralform/js 9.2.0 → 9.4.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/dist/index.cjs +134 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +170 -1
- package/dist/index.d.ts +170 -1
- package/dist/index.js +133 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
VOICE_POLISH_MODES: () => VOICE_POLISH_MODES,
|
|
39
39
|
generateId: () => generateId,
|
|
40
40
|
isEmbeddedResource: () => isEmbeddedResource,
|
|
41
|
+
isToolImageStub: () => isToolImageStub,
|
|
41
42
|
isToolOutputStub: () => isToolOutputStub,
|
|
42
43
|
isVoiceLLMMode: () => isVoiceLLMMode,
|
|
43
44
|
isVoicePolishMode: () => isVoicePolishMode,
|
|
@@ -371,6 +372,11 @@ function isToolOutputStub(value) {
|
|
|
371
372
|
const v = value;
|
|
372
373
|
return v.__stub === "tool_output" && typeof v.call_id === "string" && v.call_id.length > 0;
|
|
373
374
|
}
|
|
375
|
+
function isToolImageStub(value) {
|
|
376
|
+
if (typeof value !== "object" || value === null) return false;
|
|
377
|
+
const v = value;
|
|
378
|
+
return v.__stub === "tool_image" && typeof v.call_id === "string" && v.call_id.length > 0 && typeof v.index === "number" && Number.isInteger(v.index) && v.index >= 0;
|
|
379
|
+
}
|
|
374
380
|
|
|
375
381
|
// src/client.ts
|
|
376
382
|
var DEFAULT_BASE_URL = "https://api.astralform.ai";
|
|
@@ -494,6 +500,79 @@ var AstralformClient = class {
|
|
|
494
500
|
`/v1/code/projects/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`
|
|
495
501
|
);
|
|
496
502
|
}
|
|
503
|
+
},
|
|
504
|
+
/**
|
|
505
|
+
* Code groups: a user-named container under one repository, for a feature
|
|
506
|
+
* whose work outlives a single session. Its sessions share one plan and one
|
|
507
|
+
* set of notes, so what one writes the others read.
|
|
508
|
+
*
|
|
509
|
+
* A group is a LABEL, not a binding — unlike a task's project, which is
|
|
510
|
+
* written once on the first turn. A task may join, move between, or leave
|
|
511
|
+
* groups at any time, and a deleted group's sessions survive as ungrouped.
|
|
512
|
+
*
|
|
513
|
+
* The server enforces three things worth knowing before you render a
|
|
514
|
+
* failure: a group that is not this user's answers `404` (never `403`, so a
|
|
515
|
+
* group id cannot be probed); filing a task into a group under a DIFFERENT
|
|
516
|
+
* repository than the task's answers `409`; and the group's plan and notes
|
|
517
|
+
* are deleted with the group, while each session keeps its own.
|
|
518
|
+
*/
|
|
519
|
+
groups: {
|
|
520
|
+
/**
|
|
521
|
+
* This user's groups, oldest first. Pass a repository to narrow to one
|
|
522
|
+
* repository's — the composer's picker does; the sidebar asks once and
|
|
523
|
+
* nests them itself.
|
|
524
|
+
*/
|
|
525
|
+
list: async (repository) => {
|
|
526
|
+
const query = repository ? `?repository=${encodeURIComponent(repository)}` : "";
|
|
527
|
+
const raw = await this.get(`/v1/code/groups${query}`);
|
|
528
|
+
return raw.map((g) => camelizeKeys(g));
|
|
529
|
+
},
|
|
530
|
+
/**
|
|
531
|
+
* Create a group under a repository. The repository goes through the same
|
|
532
|
+
* normaliser the Projects surface uses, so a pasted GitHub URL is accepted
|
|
533
|
+
* and anything that is not `owner/repo` is refused.
|
|
534
|
+
*/
|
|
535
|
+
create: async (repository, title) => {
|
|
536
|
+
const raw = await this.post("/v1/code/groups", { repository, title });
|
|
537
|
+
return camelizeKeys(raw);
|
|
538
|
+
},
|
|
539
|
+
/** Rename a group. A group that is not this user's answers `404`. */
|
|
540
|
+
rename: async (groupId, title) => {
|
|
541
|
+
const raw = await this.patch(`/v1/code/groups/${encodeURIComponent(groupId)}`, { title });
|
|
542
|
+
return camelizeKeys(raw);
|
|
543
|
+
},
|
|
544
|
+
/**
|
|
545
|
+
* Delete a group. Its tasks survive and become ungrouped, each keeping its
|
|
546
|
+
* own plan and notes; the group's SHARED documents are deleted with it.
|
|
547
|
+
*/
|
|
548
|
+
remove: async (groupId) => {
|
|
549
|
+
await this.del(`/v1/code/groups/${encodeURIComponent(groupId)}`);
|
|
550
|
+
},
|
|
551
|
+
/**
|
|
552
|
+
* File a task into a group. Idempotent — filing an already-filed task
|
|
553
|
+
* again is the same state, not an error. Refused with `409` when the task
|
|
554
|
+
* belongs to a different repository than the group, because it would
|
|
555
|
+
* render nowhere in the sidebar.
|
|
556
|
+
*/
|
|
557
|
+
assign: async (groupId, conversationId) => {
|
|
558
|
+
const raw = await this.post(
|
|
559
|
+
`/v1/code/groups/${encodeURIComponent(groupId)}/tasks/${encodeURIComponent(conversationId)}`,
|
|
560
|
+
void 0
|
|
561
|
+
);
|
|
562
|
+
return camelizeKeys(raw);
|
|
563
|
+
},
|
|
564
|
+
/**
|
|
565
|
+
* Take a task out of its group. The task and its documents are untouched.
|
|
566
|
+
*
|
|
567
|
+
* Deliberately not strict about WHICH group the task is in: a task that is
|
|
568
|
+
* in a different one (or in none) already satisfies "this task should not
|
|
569
|
+
* be grouped", so the call clears it rather than refusing.
|
|
570
|
+
*/
|
|
571
|
+
unassign: async (groupId, conversationId) => {
|
|
572
|
+
await this.del(
|
|
573
|
+
`/v1/code/groups/${encodeURIComponent(groupId)}/tasks/${encodeURIComponent(conversationId)}`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
497
576
|
}
|
|
498
577
|
};
|
|
499
578
|
if (isApiKeyConfig(config)) {
|
|
@@ -694,6 +773,13 @@ var AstralformClient = class {
|
|
|
694
773
|
return await response.json();
|
|
695
774
|
});
|
|
696
775
|
}
|
|
776
|
+
/** A GET whose body is binary. Read inside the race, for the reason above. */
|
|
777
|
+
async getBlob(path) {
|
|
778
|
+
return this.withDeadline(async (signal) => {
|
|
779
|
+
const response = await this.send("GET", path, void 0, signal);
|
|
780
|
+
return response.blob();
|
|
781
|
+
});
|
|
782
|
+
}
|
|
697
783
|
async post(path, body) {
|
|
698
784
|
return this.withDeadline(async (signal) => {
|
|
699
785
|
const response = await this.send("POST", path, body, signal);
|
|
@@ -913,6 +999,7 @@ var AstralformClient = class {
|
|
|
913
999
|
const params = new URLSearchParams();
|
|
914
1000
|
if (jobId) params.set("job_id", jobId);
|
|
915
1001
|
if (options?.toolOutputs === "stub") params.set("tool_outputs", "stub");
|
|
1002
|
+
if (options?.toolImages === "stub") params.set("tool_images", "stub");
|
|
916
1003
|
const query = params.toString();
|
|
917
1004
|
return this.get(
|
|
918
1005
|
`/v1/conversations/${encodeURIComponent(conversationId)}/events${query ? `?${query}` : ""}`
|
|
@@ -927,6 +1014,25 @@ var AstralformClient = class {
|
|
|
927
1014
|
);
|
|
928
1015
|
return res.output;
|
|
929
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* The image behind a {@link ToolImageStub}, as a `Blob` of the preview's bytes.
|
|
1019
|
+
*
|
|
1020
|
+
* Takes the stub itself and nothing else — the `job_id` scoping that
|
|
1021
|
+
* `getToolOutput`'s id form warns about cannot be dropped from a value that
|
|
1022
|
+
* carries it. There is deliberately no by-ids form.
|
|
1023
|
+
*
|
|
1024
|
+
* A `Blob` rather than a URL because the route is authenticated: an `<img
|
|
1025
|
+
* src>` cannot send the bearer token, so a browser consumer turns this into
|
|
1026
|
+
* an object URL (`URL.createObjectURL`) and revokes it when the image
|
|
1027
|
+
* unmounts. The response is `private, immutable`, so a repeat fetch of the
|
|
1028
|
+
* same preview is served by the HTTP cache.
|
|
1029
|
+
*/
|
|
1030
|
+
async getToolImage(conversationId, stub) {
|
|
1031
|
+
const query = stub.job_id ? `?job_id=${encodeURIComponent(stub.job_id)}` : "";
|
|
1032
|
+
return this.getBlob(
|
|
1033
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}/tool-output/${encodeURIComponent(stub.call_id)}/images/${stub.index}${query}`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
930
1036
|
async submitToolResult(request) {
|
|
931
1037
|
await this.post("/v1/tool-result", request);
|
|
932
1038
|
}
|
|
@@ -2801,6 +2907,12 @@ var StreamManager = class _StreamManager {
|
|
|
2801
2907
|
* Defaults to inline, so this is inert until a consumer opts in.
|
|
2802
2908
|
*/
|
|
2803
2909
|
this.toolOutputs = "inline";
|
|
2910
|
+
/**
|
|
2911
|
+
* Whether restore asks for tool image previews inline or as fetch handles.
|
|
2912
|
+
* Separate from `toolOutputs` because the server keeps them separate — see
|
|
2913
|
+
* `ToolImageStub` — and on the same one-setting-both-waves rule.
|
|
2914
|
+
*/
|
|
2915
|
+
this.toolImages = "inline";
|
|
2804
2916
|
this._state = "idle";
|
|
2805
2917
|
this._activeConversationId = null;
|
|
2806
2918
|
this._backgroundJobs = /* @__PURE__ */ new Map();
|
|
@@ -2857,6 +2969,25 @@ var StreamManager = class _StreamManager {
|
|
|
2857
2969
|
setToolOutputMode(mode) {
|
|
2858
2970
|
this.toolOutputs = mode;
|
|
2859
2971
|
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Ask restore for stubbed image previews, fetched on demand.
|
|
2974
|
+
*
|
|
2975
|
+
* The same caution as `setToolOutputMode`, sharper: a consumer that cannot
|
|
2976
|
+
* resolve one (`isToolImageStub`, `client.getToolImage`) finds an `images`
|
|
2977
|
+
* entry with no `url` and draws a broken image where the preview was.
|
|
2978
|
+
*/
|
|
2979
|
+
setToolImageMode(mode) {
|
|
2980
|
+
this.toolImages = mode;
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* The ONE options object both event waves fetch with — the newest page and
|
|
2984
|
+
* every `loadEarlierTurns` page. Built in one place so an opt-in added here
|
|
2985
|
+
* reaches both by construction: two literals at two call sites is how one
|
|
2986
|
+
* wave would silently keep asking for inline payloads.
|
|
2987
|
+
*/
|
|
2988
|
+
get eventFetchOptions() {
|
|
2989
|
+
return { toolOutputs: this.toolOutputs, toolImages: this.toolImages };
|
|
2990
|
+
}
|
|
2860
2991
|
emit(event) {
|
|
2861
2992
|
for (const handler of this.handlers) {
|
|
2862
2993
|
try {
|
|
@@ -3357,9 +3488,7 @@ var StreamManager = class _StreamManager {
|
|
|
3357
3488
|
const boundary = this.oldestDrawnMessageId;
|
|
3358
3489
|
const eventLists = await Promise.all(
|
|
3359
3490
|
page.jobs.map(
|
|
3360
|
-
(job) => session.client.getConversationEvents(conversationId, job.job_id,
|
|
3361
|
-
toolOutputs: this.toolOutputs
|
|
3362
|
-
}).catch(() => [])
|
|
3491
|
+
(job) => session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
|
|
3363
3492
|
)
|
|
3364
3493
|
);
|
|
3365
3494
|
if (stop()) return 0;
|
|
@@ -3480,9 +3609,7 @@ var StreamManager = class _StreamManager {
|
|
|
3480
3609
|
});
|
|
3481
3610
|
const eventLists = await Promise.all(
|
|
3482
3611
|
replayableJobs.map(
|
|
3483
|
-
(job) => this.session.client.getConversationEvents(conversationId, job.job_id,
|
|
3484
|
-
toolOutputs: this.toolOutputs
|
|
3485
|
-
}).catch(() => [])
|
|
3612
|
+
(job) => this.session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
|
|
3486
3613
|
)
|
|
3487
3614
|
);
|
|
3488
3615
|
if (stopReplay()) return false;
|
|
@@ -3631,6 +3758,7 @@ function parseEmbeddedResource(value) {
|
|
|
3631
3758
|
VOICE_POLISH_MODES,
|
|
3632
3759
|
generateId,
|
|
3633
3760
|
isEmbeddedResource,
|
|
3761
|
+
isToolImageStub,
|
|
3634
3762
|
isToolOutputStub,
|
|
3635
3763
|
isVoiceLLMMode,
|
|
3636
3764
|
isVoicePolishMode,
|